diff --git a/ROADMAP.md b/ROADMAP.md index 48090e1..89a8ecf 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # Mira roadmap -**Latest October26 direction:** #91 is narrowed to original reviewed16ff136/generated3ad91ff. Its22/24 versus main21/24, shared final next-step failures and unchanged required-person controls meet the corrected relative comparison gate. The later policy/selection expansion and its candidate-only final factual error are withdrawn and preserved. Source parity, all96 saved request hashes/stages,1,244 full and8 no-model checks pass without quota. Final restored-diff review/current-head CI precede the authorized merge commit. Then integrate#90: frozen mapping finds shared custody/cloud families but candidate-only staff flagging, rejected by#91 in offline replay. No date-order cause is established; the conditional96-request grant is inactive. Livea9c9508/914a38f0 is operator-confirmed fromOctober26 18:52IST; earliest next releaseOctober27 18:52:34IST, with fresh rollback/off-state checks. Scores64/100 implementation and56/100 delivery remain unchanged; no new memory capability or customer resolution is claimed. +**Current October28:** #91 merged a461f86. Its exact rollout c42efc65 passed startup and the normal how-to smoke. The broken-device reply preserved earlier restarts and delivered the full case privately, but applied Limitless-specific guidance without identifying the hardware. Independent review classified that as failed source applicability; rollback toa9c9508/a6ef6b22 is verified healthy and the third smoke held. #90 integrates #91:1,258/offline8 and independent240-check review pass; final-head review/CI precede merge. This does not fix unknown source applicability or produce a new generated score. Hold further releases while #83 receives a general offline repair and required review; no paid run, memory enablement or customer-resolution claim. Scores64/100 implementation and56/100 delivery remain unchanged. **Earlier October26 checkpoint:** #86 merged647b549 after1202/no-model8, exact-head hosted Node20/24/real PostgreSQL, independent review and merged-main CI. [Lifecycle details](docs/GITHUB_NOTICE_LIFECYCLE.md). The old PR backlog is cleared; new work is Mira repository presentation and local #83 source provenance/conflict handling. #62 stays open for reproduced narration/context loss despite merged diagnostics. Production stays c6f482d; no paid run, public Omi test issue or memory enablement. Main implementation64/100; delivery56/100;44 left. Technical identifiers remain unchanged under the Mira product name. diff --git a/commandcode.js b/commandcode.js index 98e8ad2..a4d4000 100644 --- a/commandcode.js +++ b/commandcode.js @@ -8,6 +8,7 @@ const { SUPPORT_VOICE } = require('./supportVoice'); const { normalizeSubQuestions } = require('./retrieval'); const { isPreviousPost, splitHistory, previousContextBlock } = require('./supportContextFormat'); const { recordStage } = require('./answerStageEvidence'); +const { SOURCE_DATE_POLICY } = require('./sourceProvenance'); const { stripPingNarration } = require('./utils'); const { removedAnswerUnits, rememberRemovalAudit } = require('./answerFilterAudit'); @@ -668,6 +669,7 @@ async function reviewAnswer({ question, threadHistory = [], draft, removedBySafe content: `${SUPPORT_VOICE}\n\nClarification: the ban on steps from static fallback applies only to troubleshooting a tech or firmware symptom. For a how-to FAQ, the Omi team static product facts are usable below the Help Center and docs when not contradicted; do not invent a source URL. For refund, billing, subscription, account, and data requests, answer any supported policy or procedure from official pages while keeping escalation for the human action. General policy pages cannot verify a specific order status, location, tracking, or delivery date; remove or reject any such claim unless a verified order lookup tool fact supports it. If understanding.dataLossRisk is true, retain a brief warning in the customer language not to reinstall, log out, or clear local recordings while staff checks; do not add an English warning to a non-English reply. Use the customer language and script throughout, including a localized source label; romanized Hindi stays romanized. Do not append an English Source line to an existing localized citation.${handoffPlanned ? ` ${PLANNED_HANDOFF_GUIDANCE}` : ''}`, }, { role: 'system', content: PUBLIC_REPLY_PRIVACY_POLICY }, + { role: 'system', content: SOURCE_DATE_POLICY }, ...(safetyAuditTruncated ? [{ role: 'system', content: 'Some removed draft text exceeded the bounded audit budget and is unavailable. Do not infer its contents or assume the draft covered every requested point. Use only the supplied conversation and official evidence.' }] : []), ...(citationLimit === 4 ? [{ role: 'system', diff --git a/db.js b/db.js index 7339a48..2b9742c 100644 --- a/db.js +++ b/db.js @@ -1,6 +1,7 @@ const fs = require('fs'); const { Pool } = require('pg'); const { chunkDocument, cleanDocument, queryTerms, sourceAuthority } = require('./retrieval'); +const { normalizeProvenance } = require('./sourceProvenance'); function sslFor(connectionString) { const value = String(connectionString || ''); @@ -51,6 +52,7 @@ async function initSchema() { body TEXT NOT NULL DEFAULT '', fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); + ALTER TABLE doc_pages ADD COLUMN IF NOT EXISTS provenance JSONB NOT NULL DEFAULT '{}'::jsonb; CREATE TABLE IF NOT EXISTS doc_chunks ( url TEXT NOT NULL REFERENCES doc_pages(url) ON DELETE CASCADE, @@ -110,7 +112,7 @@ async function initSchema() { const chunks = await client.query(`SELECT count(*)::int AS n FROM doc_chunks`); const pages = await client.query(`SELECT count(*)::int AS n FROM doc_pages`); if ((chunks.rows[0]?.n || 0) === 0 && (pages.rows[0]?.n || 0) > 0) { - const existing = await client.query(`SELECT url, title, body FROM doc_pages`); + const existing = await client.query(`SELECT url, title, body, provenance FROM doc_pages`); for (const page of existing.rows) { await replaceDocChunks(client, page); } @@ -232,21 +234,22 @@ async function replaceDocChunks(client, page) { return chunks.length; } -async function saveDocPage({ url, title, body }) { +async function saveDocPage({ url, title, body, provenance }) { if (!url) return; const page = { url: String(url), title: String(title || ''), body: cleanDocument(body), + provenance: normalizeProvenance(provenance), }; const client = await pool.connect(); try { await client.query('BEGIN'); await client.query( - `INSERT INTO doc_pages (url, title, body, fetched_at) - VALUES ($1, $2, $3, NOW()) - ON CONFLICT (url) DO UPDATE SET title = $2, body = $3, fetched_at = NOW()`, - [page.url, page.title, page.body] + `INSERT INTO doc_pages (url, title, body, fetched_at, provenance) + VALUES ($1, $2, $3, NOW(), $4::jsonb) + ON CONFLICT (url) DO UPDATE SET title = $2, body = $3, fetched_at = NOW(), provenance = $4::jsonb`, + [page.url, page.title, page.body, JSON.stringify(page.provenance)] ); await replaceDocChunks(client, page); await client.query('COMMIT'); @@ -265,9 +268,9 @@ async function searchDocPages(question, limit = 8, sources = []) { const wantedSources = Array.isArray(sources) ? sources.map(String).filter(Boolean) : []; const { rows } = await pool.query( `WITH query AS (SELECT websearch_to_tsquery('english', $1) AS value) - SELECT title, url, section, source, authority, body, chunk_index, + SELECT c.title, c.url, c.section, c.source, c.authority, c.body, c.chunk_index, p.provenance, ts_rank_cd(search_vector, query.value, 32) AS rank - FROM doc_chunks, query + FROM doc_chunks c JOIN doc_pages p ON p.url=c.url CROSS JOIN query WHERE search_vector @@ query.value AND (cardinality($3::text[]) = 0 OR source = ANY($3::text[])) ORDER BY (ts_rank_cd(search_vector, query.value, 32) * (1 + authority * 0.06)) DESC, diff --git a/docs/ANSWER_QUALITY_CONTRACT.md b/docs/ANSWER_QUALITY_CONTRACT.md index 9039e05..362ee67 100644 --- a/docs/ANSWER_QUALITY_CONTRACT.md +++ b/docs/ANSWER_QUALITY_CONTRACT.md @@ -4,7 +4,7 @@ Updated October 26, 2026. This is an implementation contract, **not a claim that ## Priority and status -The [master plan](SUPPORT_MASTER_PLAN.md) takes precedence. The old PR queue is cleared; #71/#75/#82 closed unmerged with unresolved requirements retained. #90 remains draft on accuracy HOLD after one approved controlled comparison. The local [#62 narration-context repair](NARRATION_CONTEXT.md) has passing offline checks but still needs final independent review/current CI and separately approved generated quality. Preserve the maximum of three open PRs; do not treat a repeated one-run approval as new quota authority. +The [master plan](SUPPORT_MASTER_PLAN.md) takes precedence. The old PR queue is cleared; #71/#75/#82 closed unmerged with unresolved requirements retained. #91 context preservation is merged at a461f86; its rollout failed source applicability and was rolled back. Context/delivery passed, but that does not waive the failed grounding check. #90 provenance integrates that main and awaits final review/CI; the frozen staff-flagging defect is rejected by both integrated #90 and current main. Shared unresolved-next-step/applicability weaknesses remain #83; no new paid run is authorized by these implementation checks. Preserve the maximum of three open PRs. | Order | Customer failure to prevent | Required regression / acceptance | State | | --- | --- | --- | --- | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 01a11d7..422e019 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Mira architecture -**Current restored #62 boundary, October26:** restore the bounded transient first-filter audit→existing reviewer→existing second-look pipeline exactly to reviewed16ff136/generated3ad91ff. The later unknown-answer policy/shared-selection expansion is withdrawn, not an established factual-applicability remedy. Runtime/config identity and all96 saved request bodies plus selected stages/decisions match;1,244 full/offline8 pass. Original22/24 versus main21/24 meets the adopted relative comparison gate: no candidate-only material final failure, no tested protected-person regression and no lower pass count. Shared next-step weakness remains#83; v2's23/24 belongs only to the withdrawn expansion. Current-head review/CI precede merge. Livea9c9508/914a38f0 is operator-confirmed fromOctober26 18:52IST, optional/automatic deploy off. Earliest new releaseOctober27 18:52:34IST with fresh rollback/identity checks. No new private capability, model stage, provider quota or memory enablement. +**Current October28:** #91 merged a461f86. Its exact rollout c42efc65 passed startup and the normal how-to smoke. The broken-device reply preserved earlier restarts and delivered the full case privately, but applied Limitless-specific guidance without identifying the hardware. Independent review classified that as failed source applicability; rollback toa9c9508/a6ef6b22 is verified healthy and the third smoke held. #90 integrates #91:1,258/offline8 and independent240-check review pass; final-head review/CI precede merge. This does not fix unknown source applicability or produce a new generated score. Hold further releases while #83 receives a general offline repair and required review; no paid run, memory enablement or customer-resolution claim. Scores64/100 implementation and56/100 delivery remain unchanged. **Earlier October26 checkpoint:** #86 merged647b549 after1202/no-model8, exact-head hosted Node20/24/real PostgreSQL, final independent review and merged-main CI. Its30-day subject-time replay guard, customer erasure and non-starvable cleanup are on main, awaiting release. [Lifecycle details](GITHUB_NOTICE_LIFECYCLE.md). Production stays c6f482d; no paid run, public Omi test issue or memory enablement. The product name is Mira; technical identifiers are stable. Separate #83 source-provenance work is local and not an accuracy claim. diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 824d484..0b72680 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1,6 +1,15 @@ # Mira implementation checklist -**October26 current state:** #86 merged647b549 after1202/no-model8, exact-head hosted Node20/24/real PostgreSQL, independent review and merged-main CI. Release/smoke remain pending. [Lifecycle details](GITHUB_NOTICE_LIFECYCLE.md). Production stays c6f482d. Mira repository presentation and #83 source provenance are separate new work; no paid run, public Omi test issue or memory enablement. +**October 26 current state:** Aryan deployed647b549 as `dba28f7d`; health/schema/Discord startup pass, one running instance, optional features/automatic deploy off. #86's authorized existing-synthetic reopen/close smoke passes with one notice per event and the issue closed again; this does not finish #51 or #85. Main `a9c9508` adds only the reviewed Mira repository guide. #83 source provenance/conflict handling is a separate draft; no paid run, new public Omi test issue, agent deployment or memory enablement. + +## #83 source provenance and conflict handling — draft + +- [x] Observe missing publisher provenance across ingestion/chunks/evidence before implementation; preserve fetch time as a separate operational field. +- [x] Carry bounded publisher dates and explicit product/surface/version scope through production persistence/search/formatting; unknown/invalid/ambiguous metadata remains unknown and an undated refresh clears stale provenance. +- [x] Add source/article-title/section/explicit-scope/date ordering only for equivalent relevance/corroboration, plus one shared applicability/conflict policy in answer and both reviewer attempts. Preserve identifier punctuation and complementary safety details; the independent generic-heading displacement is reproduced and repaired. +- [x] Fourteen original synthetic regressions pass, including full-cap topic separation, stronger-match/query-corroboration guards, policy transport and six simultaneous fetches with10-second deadlines for two-/three-part questions. No provider call or protected case. +- [x] Runtime `42619cf` passes full 1,216/no-model 8/isolated SQL, exact-head hosted Node 20/24/real PostgreSQL and independent 69-check code review plus all four original reproduction assertions. Later documentation-only heads still need their required CI/review; unchanged runtime evidence is reusable. +- [ ] Obtain authorization before the necessary generated-answer checks. A policy reaching a model is not proof of compliance. Keep #83 open and draft until its actual accuracy gates pass. ## Current audited execution — October 26, 2026 @@ -10,7 +19,7 @@ - [x] Audit12 open issues; close only speculative stored summaries #52 not planned. Preserve11 unresolved issues, existing assignments and closed-branch evidence. [Disposition](SUPPORT_ISSUE_AUDIT_2026-10-25.md). - [x] #86 lifecycle implemented locally:30-day bounded/non-starvable expiry, payload subject-time rejection, erasure of owned receipt rows with a minimal replay cutoff; red-before synthetic checks,1202/no-model8/isolated SQL pass. - [x] #86 hosted real PostgreSQL/Node20/24 CI, final independent85-test/signed HTTP/SQL/two-migration-probe review and merge647b549; merged-main CI passes. -- [ ] #86 later approved-release smoke on an existing linked synthetic issue/thread. No memory enablement or new public test issue. +- [x] #86 authorized existing-synthetic reopen/close smoke passes on `647b549` / `dba28f7d`: one notice per event, ordered open checkpoint then closure, and no extra notice at the follow-up read. Issue #23602 ends closed not planned. No memory enablement or new public test issue; [limits](GITHUB_NOTICE_LIFECYCLE.md). - [x] #88 merged884fb46 with1174/offline8/current CI/independent35 checks and unchanged reply bytes; #62 remains separate from deployment/customer resolution. - [x] Step5 preparation and revised AI-provider-disclosing notice wording approved. Publication, processor/backups and explicit enablement remain unchecked; no memory flag changed. - [ ] Then #83 general source conflicts and #65 accuracy; remaining C/D/E and #51/#53/#56 operational reliability stay required. No automatic paid evaluation, customer data use, issue assignment, merge or deployment. diff --git a/docs/README.md b/docs/README.md index a14a1f6..74d80a6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ Start with the [master plan](SUPPORT_MASTER_PLAN.md). It is the current product | [Architecture](ARCHITECTURE.md) | Customer → evidence → answer/review → delivery → staff/engineering workflow | | [Implementation checklist](IMPLEMENTATION_PLAN.md) · [Roadmap](../ROADMAP.md) | Implemented boundaries, checks and next work | | [Mira’s voice](MIRA_PERSONALITY.md) · [Answer-quality contract](ANSWER_QUALITY_CONTRACT.md) | Considerate, useful and truthful support behavior | +| [Source conflicts](SOURCE_CONFLICTS.md) | Draft publisher provenance, applicability-aware ordering and unresolved-conflict policy | ## Operations, privacy and verification diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index 97ebbe7..7672798 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -1,6 +1,6 @@ # Mira operations runbook -**Current operator receipt, October26:** successful active deployment `914a38f0-1aa6-4d69-8059-34c84a2fcd9a` of `a9c9508`, created at18:52:34 IST, is confirmed operator-triggered by Aryan. One active deployment, complete automatic-trigger reads showing off, and optional memory/context/fix-inbox controls off were checked. This is metadata verification, not a new health/smoke observation. The change since647b549 is the repository guide only. Next release must be **no earlier thanOctober27 at18:52:34 IST**, with fresh live/rollback/optional-off checks and the pinned approved GitHub commit. #91 restoration/review precedes merge; no deployment is ready now. Aryan performs the three post-deploy smokes; a failure uses the verified rollback. [Current master plan](SUPPORT_MASTER_PLAN.md) controls status. +**Current October28 operator receipt:** #91 mergeda461f868ad097ec93512aeaf111d63f4e4c96997 and was deployed asc42efc65-0efb-4ca2-adca-f5f93627b37f, created20:05:37IST. Exact merged-main CI, rollback availability/cadence/source/off-state preflight, startup/health/schema/login and one running instance passed. Normal phone-only how-to smoke passed. The second smoke preserved the symptom/earlier attempts and delivered one matching private staff card, but improperly applied Limitless-specific guidance to unidentified hardware. Independent review called this a grounding failure. Under Aryan's failure rule, deploymentRollback of verified914a38f0/a9c9508 returnedtrue. Replacementa6ef6b22-26b3-4744-ab8d-e067311ff7da/a9c9508, createdOctober28 at20:27:42IST, is SUCCESS/latest/one running; health/schema/login pass with zero matched startup failures and optional/automatic deployment controls off. Earliest cadence-only window isOctober29 at20:27:42IST; applicability repair/review remains a separate release hold. Third smoke and further releases are held. #90 is not deployed. No environment/settings changes, paid rerun, memory enablement or agent Discord posts. [Master plan](SUPPORT_MASTER_PLAN.md) controls current status. Do not delete receipt/cutoff rows to force webhook replay. The candidate ignores stale payload subject times, holds uncertain outcomes and retains a minimal customer/time cutoff after owner erasure; cleanup is mandatory even with optional features off. Fixed notice-retention errors mean pending cleanup, not erased records. An unexpected populated pre-lifecycle schema fails closed for review. Use an existing linked synthetic issue/thread for the next approved-release smoke; no new public issue. diff --git a/docs/SOURCE_CONFLICTS.md b/docs/SOURCE_CONFLICTS.md new file mode 100644 index 0000000..f0a85db --- /dev/null +++ b/docs/SOURCE_CONFLICTS.md @@ -0,0 +1,65 @@ +# Source provenance and conflicting guidance — #83 + +October28 integration of main #91; final head review/CI pending before merge. Not deployed. This extends the existing ingestion → PostgreSQL/chunks → ranking → answer → review pipeline. It does not add a model call, a second knowledge store or a URL-specific exception. Main/production and all other acceptance requirements remain in the [master plan](SUPPORT_MASTER_PLAN.md). + +## Failure and contract + +An older official page can be cited correctly while giving an outdated rule. A recent fetch does not make its contents recent, and a newer page does not automatically apply to a different product, platform or version. Source authority still controls. Missing information is not a contradiction; older complementary facts and protective warnings must remain available. + +The shared answer/review policy requires the actual topic and product/surface/version applicability to be supported before treating a newer rule as superseding a conflicting one. Otherwise retain material uncertainty and use the existing person workflow. Document dates are not deployment, release or customer-account evidence. Injected-provider tests prove policy delivery and unchanged request count, not that a live model follows the policy. + +## Publisher metadata + +Help Center ingestion reads bounded Article JSON-LD bound to the fetched page's URL. It retains explicit datePublished/dateModified and, only when declared in about:SoftwareApplication, name/operatingSystem/softwareVersion. It does not infer product scope from an article title, language, viewport or a mention in body text. Missing/ambiguous/invalid/future dates stay unknown. Multiple contradictory article metadata records fail closed; sitemap/fetch times are never promoted to publication/update dates. + +The source page stores one sanitized provenance JSON object. Chunks receive it in memory; persisted search joins it from their parent page so reindexing cannot create competing provenance copies. Refreshing a page without reliable metadata clears the old provenance. Existing pages upgrade additively to an empty object, without invented dates or changes to their bodies. Valid provenance survives the real save/search/evidence path. Non-Help-Center sources may carry explicitly declared metadata through the same schema; this draft does not manufacture dates for sources whose adapters provide none. + +Formatted evidence includes bounded publisher dates/date origin and any explicit applicability. Incomplete applicability is marked as such. These fields count inside the existing evidence-character budget; no extra page fetches or tokens are requested by a separate process. Generated-answer cost/coverage effects still need approved evaluation. + +## Conservative deterministic ordering + +Both stored fusion and local ranking can reorder their already-ranked slots when documents have: + +- the same official source kind/authority; +- the same normalized article title and section (a generic section such as Overview is insufficient); +- identical explicitly declared product, surface and version identifiers; +- comparable reliable dates (updated versus updated, or published versus published). +- equal existing relevance signals: stored rank contribution and query corroboration, or local keyword rank; source-fit remains equal too. + +Newer documents occupy the earlier slots only within that group. Other topics, authorities and scopes keep their positions; a date cannot replace a stronger match or broader corroboration across searches. Identifier punctuation is significant: version2.1 cannot become version2-1. Equal, unknown, unreliable or incomparable dates do not create preference. Rows are not deleted or declared obsolete. The fixed sourcePreference enum explains the ordering; it is not proof of semantic contradiction or customer-version applicability. + +This narrow ordering is deliberately separate from resolving a claim. The existing answer/reviewer must still establish the actual applicability and contradiction from the evidence; a heading match alone is insufficient. Incomplete or differently worded topics remain available with their dates/uncertainty rather than being silently filtered. No broad claim that every official-source conflict is automatically solved follows. + +## Checks and readiness + +Fourteen new synthetic tests cover ingestion/ambiguity, chunks, real formatted stored evidence, known-scope preference, authority/topic/surface/version exclusions, punctuation, absent/equal/unreliable/incomparable metadata, complementary safety facts, shared answer/normal+compact review policy delivery, and two-/three-part fetch guards. The independent review reproduced a generic-heading collision that displaced relevant evidence at the eight-slot cap. The repaired key includes article title/section and relevance equivalence; new guards cover that full-cap case, stronger matches and multi-query corroboration. Observed red-before records also distinguish four missing-provenance behaviors, two ordering behaviors, absent policy delivery and an identifier-normalization error. No protected inputs, customer messages or production writes are used. + +The two-/three-part checks assert all six page requests start before any completes, the per-request timeout remains 10 seconds, and existing eight-chunk/9000-character output bounds hold. The real persistence/search/formatter owners are exercised against the isolated SQL engine, including an undated refresh and idempotent schema initialization. Runtime `42619cf` passes hosted Node 20/24/real PostgreSQL CI and independent scoped code review: 69 affected checks and all four original reproduction assertions. Later documentation-only heads retain their current CI/review requirement. The evaluation harness receives provenance through the same Help Center/chunk owners; its separate in-memory/storage limitations remain in the [evaluation guide](EVALUATION.md). + +## October 26 controlled answer comparison — frozen historical hold + +Aryan approved one run with a ceiling of 288 requests. It completed all 48 answer flows using **96 requests**: twelve original fictional scenarios, two repetitions per arm, main `a9c9508` versus candidate `c870a8b` (runtime `42619cf`). Both arms used the same provider configuration and fixtures; initial starts were simultaneous and paired scenario starts differed by at most 4 ms. Both workers exited normally, source hashes stayed fixed, and saved full-window power history covers the run without a sleep/wake event. The single-use approval is consumed; unused ceiling is not authorization for another run. + +Independent grades were frozen before the randomized arm mapping was opened. All 48 flows and every saved stage were read. Main passed 19/24 contracts with five material failures; candidate passed 21/24 with three material failures. These small, fictional-case counts are **not current-product accuracy percentages or proof of improvement/causation**. The reviewer withheld approval despite the higher candidate count. + +| Candidate material failure | Saved stage finding | Required repair | +| --- | --- | --- | +| Categorical no-loss reassurance | Generated and reviewed text turn general local-pending behavior into a guarantee that a capture is not lost. | Keep intended behavior separate from verified customer state; preserve uncertainty and protective guidance. | +| Unsupported cloud-conversation state | Generated and reviewed text infer a cloud/UI outcome not established by the supplied source. | Require evidence for the actual state/visibility claim, not a plausible inference from upload behavior. | +| Invented completed staff notification | The reviewer introduces completed staff flagging with no dispatch or receipt. It survives presentation. | Enforce the distinction between a requested handoff and application-confirmed delivery throughout review/presentation. | + +Main also has material unsupported privacy/data-state claims and missing next-step/person-workflow outputs. Existing saved stages identify defects; they do not establish that the provenance change caused them. Date/version/surface handling and the German context survived the candidate review; final restored custody warnings were credited. Intermediate warning removal was not mislabeled as final warning loss. + +The controlled path exercises real fixture ingestion/chunks/ranking/evidence, answer generation, draft safety, reviewer/second-look ownership, presentation and final custody-warning restoration. A supplied neutral understanding replaces planning. It excludes live documentation, the full index-handler's late redirect/delivery composition and gateway/staff dispatch. No customer or protected input was used. It is not a production or real-Omi-fact check. + +Private evidence retains the original manifest, both source snapshots, all stages and the pre-unblinding grades. Manifest SHA256: `3279438a6d1ccac0024ff440fa6039ebe2896da1d11aa9fad7b40fbb8bbcd402`; independent grades SHA256: `358796a2d8bc18bec5812f48eceb6a110ebd55dfb4d2b629bd8ce536ccf88183`. The original candidate archive verifies all 156 manifest file hashes. No generated case text or credentials are published here. + +## October28 integration and relative-gate decision + +Frozen mapping: no-loss q06-r1-B shares the broader custody-assurance family with main q03-r1-B, not an identical same-scenario guarantee. Unsupported cloud/UI state q06-r2-A shares main q06-r1-A. Completed staff flagging q07-r2-A is absent from all24 old-main finals. Its evidence is byte-identical and undated; no date-order cause is established. + +Main #91 (`a461f86`) is now integrated with a merge commit, retaining both source policy and removal-audit propagation. Full1,258/no-model8 pass. Independent review passes240 focused checks: original provenance/db/ranking code matches reviewedb9d7274; #91 safety/routing/handler code matches current main; only source policy import/system text differs in the conflict-resolved reviewer. + +Fresh offline injection of the exact frozen unsafe reviewer response through both normal and compact paths confirms old main/frozen#90 accept it while current main/integrated#90 remove/audit the invented flag, invalidate approval and invoke the existing second look. Repeated unsafe reviews stay rejected/escalated. This establishes an inherited acceptance gap now blocked for this observed defect. It does not prove that prompt changes had no influence on stochastic wording; no provenance-specific cause or integrated generated accuracy rate is claimed. + +The independent reviewer recommends merge after final-head review and Node20/24/real PostgreSQL CI. Aryan's conditional max96-request run is not activated; no paid run occurred. Keep the frozen21/24 versus19/24 and all original grades unchanged. #83 stays open for the remaining email-only/no-next-step and unsupported applicability problems, addressed offline next with general red-before tests. #65 remains unfinished. #90 release is held after #91 rollout failed source applicability and rollback was requested. Its provenance foundation does not establish customer hardware scope; #83 offline work and required review precede another rollout. Memory remains off. diff --git a/docs/SUPPORT_MASTER_PLAN.md b/docs/SUPPORT_MASTER_PLAN.md index 79946d5..fe0c3a5 100644 --- a/docs/SUPPORT_MASTER_PLAN.md +++ b/docs/SUPPORT_MASTER_PLAN.md @@ -1,6 +1,6 @@ # Mira: master product plan -Updated October 26, 2026 (IST), after the final #91 follow-up review. Current GitHub main is `a9c9508ac9be03a2bfd0f571e90f6779b6d96a48`; the fixed milestone foundation audit remains recorded against `647b549bade498573e6fb2a16bf4544377884b1e`. The repository-guide update adds no completed milestone; evidence hooks alone do not complete an additional product requirement. Aryan sets product priorities; this is the single current product checklist. The product is Mira; technical repository/deployment/package identifiers remain unchanged. +Updated October 28, 2026 (IST). #91 merged as `a461f868ad097ec93512aeaf111d63f4e4c96997` and was deployed as `c42efc65-0efb-4ca2-adca-f5f93627b37f`, then rolled back; normal how-to smoke passed, but the broken-device smoke failed source applicability and rollback to a9c9508/a6ef6b22 is verified healthy; the third smoke is held. Current GitHub main is `a461f868ad097ec93512aeaf111d63f4e4c96997`; the fixed milestone foundation audit remains recorded against `647b549bade498573e6fb2a16bf4544377884b1e`. The repository-guide update adds no completed milestone; evidence hooks alone do not complete an additional product requirement. Aryan sets product priorities; this is the single current product checklist. The product is Mira; technical repository/deployment/package identifiers remain unchanged. ## Current score — 56 / 100 done, 44 / 100 left @@ -23,12 +23,12 @@ There is **no defensible current-main “accuracy out of 100”** from these tes Require **no candidate-only material final-answer failure**, **no protected-handoff regression**, and **candidate pass count not below main**. Zero material failures is not the gate. Track inherited weaknesses separately. A main draft later corrected by review is not the same final failure. Preserve frozen grades; record the new gate assessment separately. -## Current draft decisions +## Current PR decisions | Draft | Evidence and disposition | Remaining work | | --- | --- | --- | -| [#91 — context preservation](https://github.com/BasedHardware/omi-vector-bot/pull/91), #62 | Withdraw the later unknown-answer policy/selection expansion and restore exact reviewed runtime `16ff136`, generated as `3ad91ff`. Its **22/24 versus main 21/24** meets the relative comparison gate: both remaining material failures share main's final missing-next-step family; all 12 required-person controls per arm stay true. Full 1,244 and no-model 8 pass. | Final restored-diff review and current-head CI before the authorized merge commit. No claim that source applicability is fixed. | -| [#90 — source provenance](https://github.com/BasedHardware/omi-vector-bot/pull/90), #83 | Frozen **21/24 versus main 19/24**; mapping below is complete, with no new run. | Integrate main after #91, resolve overlap, run full tests/CI and get independent integration review. The exact candidate-only staff claim is rejected by #91 in offline replay; that is diagnostic evidence, not regenerated quality approval. | +| [#91 — context preservation](https://github.com/BasedHardware/omi-vector-bot/pull/91), #62 | **Merged `a461f86`; attempted rollout `c42efc65` rolled back after smoke failure.** Exact original reviewed runtime restored; 22/24 versus main 21/24, no candidate-only material final failure or tested required-person regression. Final independent review, 1,244 full/8 no-model and head/merged-main Node20/24/PostgreSQL CI pass. | Normal how-to passed. Symptom/earlier-attempt preservation and private delivery passed, but the answer applied Limitless-only guidance to unidentified hardware. Independent review called the smoke failed; rollback to a9c9508/a6ef6b22 verified healthy. Third smoke held; fix #83 applicability before another release. | +| [#90 — source provenance](https://github.com/BasedHardware/omi-vector-bot/pull/90), #83 | Frozen 21/24 versus old main 19/24; mapping below complete. New main with #91 integrated via merge commit; preserve both source policy and first-filter audit. Full 1,258 and no-model 8 pass. Independent integration code review and 240 focused checks pass. | Final-commit review and current Node20/24/PostgreSQL CI, then authorized merge. No paid rerun: current main and integrated #90 both reject the observed candidate-only staff claim. Frozen counts are not a fresh integrated accuracy score. | ### #90 frozen final-answer failure mapping @@ -40,7 +40,9 @@ Require **no candidate-only material final-answer failure**, **no protected-hand The withdrawn #91 follow-up remains a failed attempt: 23/24 versus main 21/24, with one unsupported definite interpretation in the candidate final. Main made the similar inference only in its draft and corrected it before final. Aryan chose **fix before merging**, rather than an exception. The narrower restored implementation uses its own **22/24** evidence, never v2's score. Its 77 non-test runtime/configuration/data/dependency files match the original manifest; offline replay matches all **96 request-body hashes** and all intermediate/final audit, selection and actual decision fields. No provider call or regrading occurred. [Full preserved history and limits](NARRATION_CONTEXT.md). -**Quota:** previous #90 and both #91 grants are consumed. No new #91 paid run is authorized. Aryan conditionally permits **one #90 verification, maximum 96 total requests**, after a candidate-only material failure is plausibly linked to provenance/date changes and receives one targeted general red-before fix. That condition is not established; the grant is inactive. No unused ceiling or repeated reply authorizes a rerun. +**October28 integration decision:** the saved completed-staff-flagging acceptance gap exists in both old main and frozen #90 when given the identical unsafe review. Fresh normal/compact replay through current main and integrated #90 removes/audits that claim, invalidates approval and uses the existing single second look; repeated unsafe responses stay rejected/escalated. Independent review recommends merge after final gates. This demonstrates a pre-existing acceptance gap repaired by #91, not proof that the added prompt had no stochastic influence or a new generated-quality measurement. No provenance-specific cause is established; preserve the shared custody/state/applicability weaknesses under #83. + +**Quota:** previous #90 and both #91 grants are consumed. No new #91 paid run is authorized. Aryan conditionally permits one #90 verification, maximum 96 requests, only following a plausibly change-caused candidate-only material failure and one general red-before repair. Integration review does not establish that condition and does not activate the grant. No provider call or rerun occurred. ## What is on main, what is live, what is not @@ -48,13 +50,13 @@ The withdrawn #91 follow-up remains a failed attempt: 23/24 versus main 21/24, w | --- | --- | | **On main** | Concise support voice/routing/retrieval/clipping; #48 confidence/source-fit foundation; #59 calm practical AI-labelled voice; #41 staff case/pickup lifecycle; #60 localized closure; #76 request limits and #70 known-unsent recovery; #67/#80 in-thread/redirect cleanup; #81 localized rejected-review copy; #28 durable private engineering approvals; #84 application-owned public-intake privacy. | | **On main, disabled** | #61 references-only memory and bounded context, #73 SQL/reader safety rehearsal, #74 owner-scoped correction-link forgetting, #49 reviewed-fact/revocation foundation. Merged code is not permission to use customer history or publish facts. | -| **Last verified production** | Exact `a9c9508`, deployment `914a38f0`, created October 26 at 18:52:34 IST and confirmed operator-triggered by Aryan. Railway metadata reports SUCCESS and one active deployment; automatic deployment triggers and optional controls are off. This is metadata verification, not a fresh health/smoke claim. #89 is a repository guide; this deployment adds no behavior beyond647b549. | +| **Release rolled back; baseline verified** | #91 exact a461f86/c42efc65 passed startup but failed the broken-device source-applicability smoke. Verified rollback replacementa6ef6b22 runs baselinea9c9508, healthy/latest/one running with schema/login ready. Normal how-to and context/private-delivery portions passed; do not call the release accepted. Third smoke held. Optional/automatic deployment controls remain off. | | **Passed limited release checks** | October25 phone-only how-to, damaged-item privacy, order-status privacy, and exact staff-authored sanitized private engineering preview. These are bounded scenarios, not full A–E acceptance. | | **Separate operator action** | Aryan clicked Publish after the preview stop point. Synthetic issue #23602 was created and, with his separate approval, closed not planned with a test-only note. Preserve history; no real customer incident, automatic publication or customer-resolution claim. | | **Deployed, scoped notice smoke passed** | [#86](https://github.com/BasedHardware/omi-vector-bot/pull/86), merge `647b549`: **Receipt lifecycle approved October26:**30 days, original payload-subject-age rejection and customer erasure. Full 1,202/no-model 8, exact-head hosted Node 20/24/real PostgreSQL and independent 85-test/signed HTTP/SQL/two-migration-probe review passed. Aryan deployed it; health/startup pass. He explicitly approved a reopen/verify/close/verify round trip on existing synthetic issue #23602. One notice per event, ordered open checkpoint then closure, final issue closed not planned and no extra notice at the follow-up read. This does not prove every replay/external-send outcome. [Lifecycle and limits](GITHUB_NOTICE_LIFECYCLE.md). No implicit memory enablement. | | **Closed without merge** | #71, #75 and #82 failed their final accuracy/execution disposition; #29/#30/#68/#77/#78 were closed with remaining scope/evidence retained. Do not reopen or rerun them automatically. #65/#51/#56/#62 keep the unresolved needs. | -The earlier PR backlog is cleared, not the full product backlog. #88 merged884fb46; #87 merged928676b; #86 merged647b549 after all required checks/review. Aryan's647b549/dba28f7d rollout and scoped #86 smoke are verified separately. #89's Mira repository guide is on maina9c9508. New work is #90/#83 source provenance and #62 narration/context preservation; both remain unmerged. Stay within the standing maximum of three open PRs, including ready PRs; every new PR starts draft. No count-driven merge or scope loss. Runtime/notice wording approvals do not grant deployment, settings, assignment, paid reruns or memory enablement. +The earlier PR backlog is cleared, not the full product backlog. #88 merged884fb46; #87 merged928676b; #86 merged647b549 after all required checks/review. Aryan's647b549/dba28f7d rollout and scoped #86 smoke are verified separately. #89's Mira repository guide is on maina9c9508. #91 context preservation is merged; its rollout failed the source-applicability smoke and was rolled back; #90 provenance is integrated and awaiting its final gates. Broader #83/#65 work remains. Stay within the standing maximum of three open PRs, including ready PRs; every new PR starts draft. No count-driven merge or scope loss. Runtime/notice wording approvals do not grant deployment, settings, assignment, paid reruns or memory enablement. ## Issues — audited decisions, not count-driven closure @@ -68,16 +70,16 @@ The earlier PR backlog is cleared, not the full product backlog. #88 merged884fb ## Next steps — accuracy first, then controlled enablement -1. **Finish and merge the narrowed #91 after its gates.** Verify exact runtime/source parity with16ff136/generated3ad91ff, preserve its first relative comparison, review the restored diff and pass current-head CI. Use a merge commit. Keep the withdrawn expansion and failed result in history/backlog; no assumed factual repair. -2. **Integrate #90 and decide from evidence.** Merge new main into the existing draft after #91. Resolve conflicts, replay its candidate-only staff claim through the integrated production reviewer, then run full tests/CI and independent integration review. Use the conditional 96-request verification only if its stated cause/fix conditions are met. No automatic reruns. The shared #83 claim-to-source applicability investigation continues on main after #91 with general red-before tests, no prompt-tweak loop and no assumed schema remedy. +1. **Finish #91 release acceptance.** Merge is complete; release is rejected and rollback baseline health/startup is verified. Fix the general applicability boundary under #83. Preserve the passing how-to and context/delivery evidence without presenting them as whole-release acceptance. Do not request the third smoke on the rejected image. +2. **Finish #90 final gates and merge.** Integration, 1,258/8 checks and independent code/replay review pass; final head review/CI remain. Further release is held after the failed #91 smoke. Complete #83 applicability work/review and fresh release timing/exact-SHA/rollback/off-state gates before shipping anything. Start #83 offline next: general unresolved-next-step and conflicting-source applicability boundaries with synthetic red-before tests, no paid run or assumed model self-certification. 3. **Return to multipart phone-only accuracy (#65, A2).** #71/#75/#82 remain closed unmerged; do not reopen or rerun them automatically. Preserve their evidence and unfinished requirements. General routing/retrieval/coverage changes belong in the same answer/review pipeline, with no claim that #62 context repair alone resolves multipart accuracy. 4. **Complete memory preparation before a separately approved pilot (C1–C4).** The [pilot checklist](MEMORY_PILOT_PREPARATION.md) is drafted: test forum only, references rather than copied messages, 30 days, owner erasure, switch/restart timing, two-post scenario, 48 clean hours and rollback. Revised provider-disclosing notice wording is approved but not published; #86's separately approved existing-issue notice smoke passed. Processor/backup/privacy checks, approved isolated runtime/release and Aryan's explicit enable instruction still precede any saving or use of history. No pilot has begun. 5. **Build reviewed improvements through the same pipeline (D1–D6, then E1–E3).** Minimized owner signals → safe grouping/private review → official-source and regression validation → authorized fact publication/revocation → offline nightly checks and visible failures → honest weekly outcome reporting → the full end-to-end demonstration. Existing disabled stores are foundations, not completed D requirements or permission to publish/train. 6. **Retain the reliability and operations dependencies.** #51 uncertain-send/outbox reconciliation, #53 staff queue/coverage, #56 capability/failure and invalid-generation diagnostics, and #85 retention/erasure remain open at their actual stages. Integrate each into the existing jobs, approvals, knowledge and delivery owners rather than adding disconnected workflows. Parked access/security work retains its scope and authorization boundary in the issue audit. -**Release timing:** preserve the earlier647b549/dba28f7d receipt and scoped #86 smoke. The newer operator deployment is `a9c9508` / `914a38f0`, created October 26 at18:52:34 IST. Under the requested24-hour rule, **no next deployment before October 27 at18:52:34 IST**. Recheck current live/rollback identity, automatic deploys and optional-off settings at dispatch; another deployment can move the window. Pin the approved GitHub #91 release commit. Aryan performs the three sequential how-to, unresolved-product-question and symptom/earlier-attempt handoff smokes. A failed smoke uses the verified rollback. #90 ships in its subsequent approved release, not as an unreviewed replacement for the pinned #91 release. +**Release timing:** #91 rollout a461f86/c42efc65 failed the broken-device smoke: Limitless-specific guidance was presented without identifying the customer hardware. Rollback to a9c9508 was explicitly requested under the approved failure rule. Replacementa6ef6b22/a9c9508 is verified healthy, createdOctober28 at20:27:42IST; earliest cadence-only window isOctober29 at20:27:42IST, not permission to deploy. Hold further releases until the general source-applicability issue and required checks are resolved. #90 is not deployed. Never use Restart for teardown. -**Need Aryan now:** no Discord input or additional quota approval. Continue safe offline investigation first. When the relevant approved runtime and any required observer are ready, give Aryan one exact Discord input; he sends every message, command and button action. The closed synthetic issue's courtesy note remains a private draft item, never an automatic send. No memory flag until notice approval **and** an explicit enable instruction. +**Need Aryan now:** no additional Discord input; the third smoke is held after the failed second smoke. Continue #90 final gates and #83 offline work. Aryan sends every message, command and button action. No new quota, enabled memory, customer data or automatic message is requested. No memory flag until notice approval **and** an explicit enable instruction. Continue #90's final gates and #83 offline work while waiting. ## Fixed milestone ledger diff --git a/docs/SUPPORT_PROGRESS.json b/docs/SUPPORT_PROGRESS.json index 83d3010..e112fca 100644 --- a/docs/SUPPORT_PROGRESS.json +++ b/docs/SUPPORT_PROGRESS.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "auditedOn": "2026-10-26", "timezone": "Asia/Kolkata", - "auditedMain": "647b549bade498573e6fb2a16bf4544377884b1e", + "auditedMain": "a9c9508ac9be03a2bfd0f571e90f6779b6d96a48", "rubric": "25 fixed milestones; one point per main implementation, tested, independently reviewed and relevant live/tool-operation verification", "notAccuracy": true, "milestones": [ diff --git a/helpcenter.js b/helpcenter.js index 0c7598c..29ccbbc 100644 --- a/helpcenter.js +++ b/helpcenter.js @@ -2,6 +2,7 @@ const SITEMAP = 'https://help.omi.me/sitemap.xml'; const HELP_FORUM = () => String(process.env.HELP_FORUM_CHANNEL_ID || '').trim(); const { cleanDocument } = require('./retrieval'); const { redactSensitive } = require('./privacy'); +const { articleProvenance } = require('./sourceProvenance'); function articleUrls(xml) { return [...String(xml || '').matchAll(/(https:\/\/help\.omi\.me\/en\/articles\/[^<]+)<\/loc>/g)].map( @@ -52,7 +53,7 @@ async function loadHelpCenter(fetchImpl, db) { const html = await res.text(); const body = htmlToText(html); if (!body) return; - await db.saveDocPage({ url, title: titleFromHtml(html, url), body }); + await db.saveDocPage({ url, title: titleFromHtml(html, url), body, provenance: articleProvenance(html,url) }); saved += 1; } catch (err) { console.error('[Help] article failed:', err.message); diff --git a/prompt.js b/prompt.js index 6ab1f78..a28d7d8 100644 --- a/prompt.js +++ b/prompt.js @@ -2,6 +2,7 @@ const { describe } = require('./router'); const { askedWhereRecordingsWent } = require('./honesty'); const { SUPPORT_VOICE } = require('./supportVoice'); const { splitHistory, previousContextBlock } = require('./supportContextFormat'); +const { SOURCE_DATE_POLICY } = require('./sourceProvenance'); const HOWTO_FAQ = [ 'Omi works with iPhone and Android. Keep the Omi app open. If you fully close it (swipe it away), it stops writing down what was said and the device disconnects.', @@ -145,6 +146,8 @@ ${SUPPORT_COMMUNICATION_POLICY} ${PUBLIC_REPLY_PRIVACY_POLICY} +${SOURCE_DATE_POLICY} + The person asking is a customer. They may paste a rant, a screenshot dump, numbered questions, or one messy paragraph. Read it like a person would. Figure out the actual problem. Think about their message before you look at the FAQ. What did they already do? What still fails? Answer that, not a generic setup guide. diff --git a/retrieval.js b/retrieval.js index e483ffa..e748957 100644 --- a/retrieval.js +++ b/retrieval.js @@ -1,6 +1,7 @@ const MAX_DOCUMENT = 60_000; const CHUNK_SIZE = 1_400; const CHUNK_OVERLAP = 220; +const { normalizeProvenance, provenanceLine, preferNewerApplicable } = require('./sourceProvenance'); const STOP_WORDS = new Set([ 'about', @@ -139,7 +140,7 @@ function splitLongPart(part, size = CHUNK_SIZE) { return out; } -function chunkDocument({ url, title, body }, { size = CHUNK_SIZE, overlap = CHUNK_OVERLAP } = {}) { +function chunkDocument({ url, title, body, provenance }, { size = CHUNK_SIZE, overlap = CHUNK_OVERLAP } = {}) { const clean = cleanDocument(body); if (!clean) return []; const parts = clean @@ -160,6 +161,7 @@ function chunkDocument({ url, title, body }, { size = CHUNK_SIZE, overlap = CHUN body: text, source: sourceKind(url), chunkIndex: chunks.length, + ...(provenance ? { provenance:normalizeProvenance(provenance) } : {}), }); const tail = text.slice(Math.max(0, text.length - overlap)); const boundary = tail.indexOf(' '); @@ -372,6 +374,7 @@ function diverseTop(rows, limit, maxPerPage = 2) { function mergeRanked(resultSets, limit = 8, { customerQuestion = '' } = {}) { const phoneQuestion = isConsumerPhoneQuestion(customerQuestion); const merged = new Map(); + const dateRelevance = new Map(); for (const rows of resultSets || []) { (rows || []).forEach((row, index) => { const key = `${row.url || ''}#${row.chunk_index ?? row.chunkIndex ?? index}`; @@ -384,14 +387,23 @@ function mergeRanked(resultSets, limit = 8, { customerQuestion = '' } = {}) { if (prior) { prior.relevanceScore += relevanceScore; prior.fusedScore += score; - } else merged.set(key, { ...row, authority, relevanceScore, fusedScore: score }); + const evidence = dateRelevance.get(prior); + evidence.matches += 1; + evidence.rankSum += Number(row.rank || 0); + } else { + const value = { ...row, authority, relevanceScore, fusedScore: score }; + merged.set(key, value); + dateRelevance.set(value, { matches: 1, rankSum: Number(row.rank || 0) }); + } }); } const ranked = [...merged.values()] .sort((a, b) => (phoneQuestion ? b.relevanceScore - a.relevanceScore : 0) || consumerSourceFit(customerQuestion, b) - consumerSourceFit(customerQuestion, a) || b.fusedScore - a.fusedScore || b.authority - a.authority); - return diverseTop(ranked, limit); + return diverseTop(preferNewerApplicable(ranked, row => row.source || sourceKind(row.url), row => [ + dateRelevance.get(row).matches, dateRelevance.get(row).rankSum, consumerSourceFit(customerQuestion,row), + ]), limit); } function rankLocalChunks(queries, rows, limit = 8, { customerQuestion = '', subQuestions = [] } = {}) { @@ -430,9 +442,12 @@ function rankLocalChunks(queries, rows, limit = 8, { customerQuestion = '', subQ .sort((a, b) => (phoneQuestion ? b.relevanceScore - a.relevanceScore : 0) || consumerSourceFit(customerQuestion || queries?.[0], b) - consumerSourceFit(customerQuestion || queries?.[0], a) || b.rank - a.rank || b.authority - a.authority); + const preferred=preferNewerApplicable(ranked, row => row.source || sourceKind(row.url), row => [ + row.rank, consumerSourceFit(customerQuestion || queries?.[0],row), + ]); return normalizeSubQuestions(subQuestions).length > 3 - ? selectSubQuestionEvidence(ranked, subQuestions, limit, { customerQuestion: customerQuestion || queries?.[0] }) - : diverseTop(ranked, limit); + ? selectSubQuestionEvidence(preferred, subQuestions, limit, { customerQuestion: customerQuestion || queries?.[0] }) + : diverseTop(preferred, limit); } function formatEvidence(rows, { maxChars = 9_000, maxPerPage = 2, onSources } = {}) { @@ -455,7 +470,8 @@ function formatEvidence(rows, { maxChars = 9_000, maxPerPage = 2, onSources } = const section = row.section ? ` — ${row.section}` : ''; const body = cleanDocument(row.body, 1_700); if (!body) continue; - const block = `[${id} | ${sourceLabel(kind)} | ${trust}]\n${row.title || 'Untitled'}${section}\n${url}\n${body}`; + const metadata = provenanceLine(row.provenance); + const block = `[${id} | ${sourceLabel(kind)} | ${trust}]\n${row.title || 'Untitled'}${section}\n${url}\n${metadata ? metadata+'\n' : ''}${body}`; const separatorChars = blocks.length ? 2 : 0; if (used + separatorChars + block.length > maxChars && blocks.length) break; blocks.push(block); diff --git a/scripts/verify-support-db.js b/scripts/verify-support-db.js index 8361f5a..6a44ae3 100644 --- a/scripts/verify-support-db.js +++ b/scripts/verify-support-db.js @@ -293,6 +293,32 @@ async function verifyGitHubNoticeReceipts(pool, prefix) { console.log('GitHub notice receipts: concurrent claims, uncertain/lost ACK fences, subject-age rejection after pruning, customer erasure/recreation, bounded non-starvable cleanup and text-free schema passed'); } +async function verifySourceProvenance(pool) { + // Use the production persistence/search owners against this isolated database. + // This runs after legacy-upgrade fixtures; it cannot pre-upgrade their schemas. + const db = require('../db'); + const original = { connect:db.pool.connect,query:db.pool.query }; + db.pool.connect = pool.connect.bind(pool); db.pool.query = pool.query.bind(pool); + try { + await db.initSchema(); + const url = 'https://help.omi.me/en/articles/synthetic-capture-provenance'; + const provenance = { publishedAt:'2025-01-02T03:04:05.000Z',updatedAt:'2026-02-03T04:05:06.000Z', + dateOrigin:'article-jsonld',applicability:{product:'synthetic-recorder',surface:'mobile',version:'2'} }; + await db.saveDocPage({url,title:'Capture provenance',body:'Capture provenance settings are available.',provenance}); + const rows = await db.searchDocPages('Capture provenance',8,['help']); + assert.ok(rows.length > 0); assert.deepEqual(rows[0].provenance,provenance); + const evidence = await require('../docs').storedDocs('Capture provenance',db); + assert.match(evidence,/Updated: 2026-02-03T04:05:06\.000Z/); + assert.match(evidence,/surface=mobile/); + // A refresh without dates clears obsolete metadata, never promotes fetched_at. + await db.saveDocPage({url,title:'Capture provenance',body:'Capture provenance settings remain available.'}); + assert.deepEqual((await db.searchDocPages('Capture provenance',8,['help']))[0].provenance,{}); + await db.initSchema(); + assert.deepEqual((await db.searchDocPages('Capture provenance',8,['help']))[0].provenance,{}); + console.log('Official source provenance: production save/search/evidence round trip, unknown-date refresh and additive schema passed'); + } finally { db.pool.connect=original.connect; db.pool.query=original.query; } +} + async function verify(pool) { const prefix = `test-${randomUUID()}`; @@ -894,7 +920,7 @@ async function verifyKnowledgePrivacy(pool, prefix) { (async () => { const pool = await isolatedPool(); - try { await verify(pool); } finally { await pool.end(); } + try { await verify(pool); await verifySourceProvenance(pool); } finally { await pool.end(); } })().catch((error) => { // SQL and connection errors can contain inputs. Never dump them or a URL. console.error(`Support database integration failed: ${error.name}; code=${String(error.code || 'check_failed').replace(/[^A-Za-z0-9_]/g, '')}`); diff --git a/sourceProvenance.js b/sourceProvenance.js new file mode 100644 index 0000000..fe8949f --- /dev/null +++ b/sourceProvenance.js @@ -0,0 +1,111 @@ +// Public source provenance, never crawl-time freshness or inferred product facts. +const DATE_ORIGINS = new Set(['article-jsonld', 'article-meta', 'declared']); +const SOURCE_DATE_POLICY = 'Keep the existing source-authority hierarchy. Prefer a newer document over a conflicting older rule only after the same topic, product, surface and version applicability is established from the evidence. Dates, titles, missing scope and crawl times do not establish applicability. A newer lower-trust page cannot override a higher-trust source. Keep complementary instructions and safety details; omission is not contradiction. If a material conflict or applicability remains unresolved, state uncertainty and escalate through the existing person workflow instead of asserting either rule. Document dates do not prove a release, deployment or account outcome.'; +function absoluteDate(value, now = Date.now()) { + if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2}))?$/.test(value)) return null; + const date = value.slice(0,10), day = Date.parse(date + 'T00:00:00Z'); + if (!Number.isFinite(day) || new Date(day).toISOString().slice(0,10) !== date) return null; + const parsed = Date.parse(value); + return Number.isFinite(parsed) && parsed <= now + 300000 ? new Date(parsed).toISOString() : null; +} + +function normalizeProvenance(value, now = Date.now()) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return {}; + const out = {}, dates = {}; + if (DATE_ORIGINS.has(value.dateOrigin)) { + for (const field of ['publishedAt','updatedAt']) { + if (value[field] === undefined) continue; + const date = absoluteDate(value[field],now); + if (!date) return {}; + dates[field] = date; + } + if (dates.publishedAt && dates.updatedAt && dates.updatedAt < dates.publishedAt) return {}; + if (Object.keys(dates).length) Object.assign(out,dates,{dateOrigin:value.dateOrigin}); + } + const scope = {}; + for (const field of ['product','surface','version']) { + const item = value.applicability?.[field]; + if (typeof item === 'string' && /^[\p{L}\p{N} ._+:/-]{1,80}$/u.test(item) && item.trim()) scope[field] = item.trim(); + } + if (Object.keys(scope).length) out.applicability = scope; + return out; +} + +function pageIdentity(value) { + try { const url = new URL(value); url.hash = ''; return url.href; } catch { return ''; } +} + +function articleProvenance(html, url, now = Date.now()) { + const found = []; + const scripts = String(html || '').matchAll(/]*\btype\s*=\s*["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi); + let scanned = 0; + for (const match of scripts) { + if (++scanned > 16) break; + if (match[1].length > 64000) continue; + let root; + try { root = JSON.parse(match[1]); } catch { continue; } + const nodes = (Array.isArray(root) ? root : [root, ...(Array.isArray(root?.['@graph']) ? root['@graph'] : [])]).slice(0,32); + for (const node of nodes) { + const types = Array.isArray(node?.['@type']) ? node['@type'] : [node?.['@type']]; + if (!types.some(type => ['Article','TechArticle','NewsArticle','BlogPosting'].includes(type))) continue; + const identity = node.url || node.mainEntityOfPage?.['@id']; + if (!identity || pageIdentity(identity) !== pageIdentity(url)) continue; + const value = { dateOrigin:'article-jsonld' }; + if (node.datePublished !== undefined) value.publishedAt = node.datePublished; + if (node.dateModified !== undefined) value.updatedAt = node.dateModified; + // Scope is known only when the publisher explicitly names the software. + // A page's language, viewport or inferred topic is not application scope. + if (node.about?.['@type'] === 'SoftwareApplication') value.applicability = { + product:node.about.name, surface:node.about.operatingSystem, version:node.about.softwareVersion, + }; + found.push(normalizeProvenance(value,now)); + } + } + if (!found.length) return {}; + const unique = new Map(found.map(value => [JSON.stringify(value),value])); + return unique.size === 1 ? found[0] : {}; +} + +function provenanceLine(value) { + const data = normalizeProvenance(value), parts = []; + if (data.publishedAt) parts.push('Published: ' + data.publishedAt); + if (data.updatedAt) parts.push('Updated: ' + data.updatedAt); + if (data.dateOrigin) parts.push('date source=' + data.dateOrigin); + if (data.applicability) parts.push('Explicit applicability: ' + Object.entries(data.applicability).map(([key,value]) => key+'='+value).join('; ')); + if (parts.length && !['product','surface','version'].every(key=>data.applicability?.[key])) parts.push('applicability incomplete; do not infer from date'); + return parts.join(' | '); +} + +function preferNewerApplicable(rows, sourceOf, relevanceOf) { + const result=[...rows], groups=new Map(); + const normalized=value=>String(value||'').normalize('NFKC').toLowerCase().replace(/[^\p{L}\p{N}]+/gu,' ').trim(); + const scopeIdentity=value=>String(value).normalize('NFKC').trim().toLowerCase().replace(/\s+/g,' '); + result.forEach((row,index)=>{ + const source=sourceOf(row); + if (!['help','docs','github','website'].includes(source)) return; + const data=normalizeProvenance(row.provenance), scope=data.applicability; + if (!['product','surface','version'].every(key=>scope?.[key])) return; + const title=normalized(row.title), section=normalized(row.section); + if (!title) return; + const relevance=relevanceOf?.(row); + if (!Array.isArray(relevance) || !relevance.length || !relevance.every(Number.isFinite)) return; + const dateKind=data.updatedAt?'updatedAt':data.publishedAt?'publishedAt':null; + if (!dateKind) return; + // Generic section names cannot establish article topic equivalence. Dates + // also cannot replace a stronger lexical match or multi-query support. + const group=JSON.stringify([source,title,section,...['product','surface','version'].map(key=>scopeIdentity(scope[key])),dateKind,relevance]); + if (!groups.has(group)) groups.set(group,[]); + groups.get(group).push({index,row,at:data[dateKind]}); + }); + for (const group of groups.values()) { + if (group.length<2 || new Set(group.map(value=>value.at)).size<2) continue; + const ordered=[...group].sort((a,b)=>b.at.localeCompare(a.at)||a.index-b.index); + // Reorder only the already-ranked slots belonging to this proven scope. + // Nothing is removed; other authorities/topics retain their positions. + group.forEach((slot,index)=>{result[slot.index]={...ordered[index].row, + sourcePreference:'newer_same_topic_and_explicit_scope'};}); + } + return result; +} + +module.exports = { absoluteDate,normalizeProvenance,articleProvenance,provenanceLine,preferNewerApplicable,SOURCE_DATE_POLICY }; diff --git a/test/sourceProvenance.test.js b/test/sourceProvenance.test.js new file mode 100644 index 0000000..daa1ed0 --- /dev/null +++ b/test/sourceProvenance.test.js @@ -0,0 +1,230 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { loadHelpCenter } = require('../helpcenter'); +const { chunkDocument, formatEvidence, rankLocalChunks, mergeRanked } = require('../retrieval'); +const { storedDocs, relevantDocs } = require('../docs'); + +const url = 'https://help.omi.me/en/articles/synthetic-capture-settings'; +const publishedAt = '2025-01-02T03:04:05.000Z'; +const updatedAt = '2026-02-03T04:05:06.000Z'; +const provenance = { publishedAt, updatedAt, dateOrigin: 'article-jsonld', + applicability: { product: 'synthetic-recorder', surface: 'mobile', version: '2' } }; + +async function ingest(html) { + const pages = []; + await loadHelpCenter(async address => ({ ok: true, text: async () => address.endsWith('sitemap.xml') + ? `${url}2026-09-01` : html }), + { saveDocPage: async page => pages.push(page) }); + return pages[0]; +} + +test('official article dates survive ingestion without substituting crawl or sitemap time', async () => { + const page = await ingest(`Capture settings

Capture settings

Capture settings can be changed in the app.

`); + assert.deepEqual(page.provenance, { publishedAt, updatedAt, dateOrigin: 'article-jsonld' }); + const undated = await ingest('

Capture settings

Capture settings can be changed.

'); + assert.deepEqual(undated.provenance, {}, 'missing official dates stay unknown despite a recent sitemap/fetch'); +}); + +test('unrelated or conflicting structured article dates do not manufacture freshness', async () => { + const article = value => ``; + for (const metadata of [ + { url:'https://help.omi.me/en/articles/synthetic-other',dateModified:updatedAt }, + { dateModified:'yesterday' }, { dateModified:'2026-02-31T00:00:00Z' }, + { datePublished:updatedAt,dateModified:publishedAt }, + { dateModified:'2999-01-01T00:00:00Z' }, + ]) { + const page = await ingest(article(metadata) + '
Capture settings.
'); + assert.deepEqual(page.provenance, {}); + } + const ambiguous = await ingest(article({ dateModified:updatedAt }) + article({ dateModified:publishedAt }) + '
Capture settings.
'); + assert.deepEqual(ambiguous.provenance, {}, 'contradictory page metadata is not silently picked'); +}); + +test('chunks and formatted evidence preserve bounded provenance and explicit applicability', () => { + const chunks = chunkDocument({ url, title:'Capture settings',body:'Capture settings use the mobile app.',provenance }); + assert.deepEqual(chunks[0].provenance, provenance); + const evidence = formatEvidence(chunks); + assert.match(evidence, /Published: 2025-01-02T03:04:05\.000Z/); + assert.match(evidence, /Updated: 2026-02-03T04:05:06\.000Z/); + assert.match(evidence, /product=synthetic-recorder; surface=mobile; version=2/); + assert.ok(evidence.length <= 9000); +}); + +test('stored selection keeps date and scope metadata through the real evidence formatter', async () => { + const evidence = await storedDocs('How do I change capture settings?', { + searchDocPages: async (_query,_limit,sources=[]) => sources.includes('github') ? [] : [{ + url,title:'Capture settings',source:'help',body:'Capture settings use the mobile app.',chunk_index:0,rank:1,provenance, + }], + }); + assert.match(evidence, /Updated: 2026-02-03T04:05:06\.000Z/); + assert.match(evidence, /surface=mobile/); +}); + +function competingRows() { + const base = {title:'Capture pause',section:'Capture pause',source:'help',rank:1,chunk_index:0, + provenance:{...provenance,applicability:{...provenance.applicability}}}; + return [ + {...base,url:'https://help.omi.me/en/articles/synthetic-older-pause',body:'Capture pause is unavailable.', + provenance:{...base.provenance,updatedAt:'2025-02-03T04:05:06.000Z'}}, + {...base,url:'https://help.omi.me/en/articles/synthetic-newer-pause',body:'Capture pause is available in the application.'}, + ]; +} + +test('equally authoritative same-topic and explicit-scope evidence prefers the newer document without deleting either', () => { + const rows=competingRows(); + const local=rankLocalChunks(['Capture pause'],rows,8,{customerQuestion:'Explain capture pause.'}); + const stored=mergeRanked([rows],8,{customerQuestion:'Explain capture pause.'}); + for(const selected of [local,stored]) { + assert.equal(selected[0].url,rows[1].url); + assert.equal(selected.length,2); + assert.equal(selected[0].sourcePreference,'newer_same_topic_and_explicit_scope'); + } +}); + +test('date preference cannot override source authority, different topics, versions or surfaces', () => { + const variants = [ + {source:'website',url:'https://www.omi.me/pages/synthetic-pause'}, + {section:'Capture upload'}, + {provenance:{...provenance,applicability:{...provenance.applicability,version:'3'}}}, + {provenance:{...provenance,applicability:{...provenance.applicability,surface:'desktop'}}}, + ]; + for(const variant of variants) { + const [old,fresh]=competingRows(); + const selected=mergeRanked([[old,{...fresh,...variant}]],8); + assert.equal(selected[0].url,old.url); + assert.ok(selected.every(row=>!row.sourcePreference)); + } +}); + +test('generic shared headings cannot merge unrelated article topics or displace relevant evidence at the full cap', () => { + const [old,fresh]=competingRows(); + const relevant=Array.from({length:8},(_,index)=>({...old,title:`Offline capture guide ${index}`,section:'Overview', + url:`https://help.omi.me/en/articles/synthetic-offline-${index}`,rank:1-index/20, + body:'Offline capture keeps pending clips until upload completes.'})); + const unrelated={...fresh,title:'Appearance settings',section:'Overview',rank:0.001, + body:'Set a light or dark theme for the toolbar.'}; + const select=[ + (rows,limit)=>mergeRanked([rows],limit,{customerQuestion:'Offline capture'}), + (rows,limit)=>rankLocalChunks(['Offline capture'],rows,limit,{customerQuestion:'Offline capture'}), + ]; + for(const choose of select) { + assert.equal(choose([relevant[0],unrelated],1)[0].url,relevant[0].url); + const without=choose(relevant,8).map(row=>row.url); + assert.deepEqual(choose([...relevant,unrelated],8).map(row=>row.url),without); + } +}); + +test('a newer same-title document cannot override a stronger match or broader query corroboration', () => { + const [old,fresh]=competingRows(); + old.rank=10;fresh.rank=0.1; + assert.equal(mergeRanked([[old,fresh]],1)[0].url,old.url); + old.rank=1;fresh.rank=1; + assert.equal(mergeRanked([[old,fresh],[old]],1)[0].url,old.url); + old.body='Capture pause supports offline duration limits.'; + assert.equal(rankLocalChunks(['Capture pause offline duration'],[old,fresh],1)[0].url,old.url); +}); + +test('unknown, equal, unreliable and incomparable dates or applicability do not manufacture precedence', () => { + const variants=[ + {},{...provenance,updatedAt:'2025-02-03T04:05:06.000Z'}, + {...provenance,updatedAt:'yesterday'}, + {...provenance,dateOrigin:'crawl-time'}, + {publishedAt:updatedAt,dateOrigin:'article-jsonld',applicability:provenance.applicability}, + {...provenance,applicability:{surface:'mobile'}}, + ]; + for(const candidate of variants) { + const [old,fresh]=competingRows(); + const selected=mergeRanked([[old,{...fresh,provenance:candidate}]],8); + assert.equal(selected[0].url,old.url); + assert.ok(selected.every(row=>!row.sourcePreference)); + } +}); + +test('punctuation in explicit version and product identifiers never merges different applicability scopes', () => { + for (const [field,older,newer] of [['version','2.1','2-1'],['product','recorder-a','recorder a']]) { + const [old,fresh]=competingRows(); + old.provenance={...old.provenance,applicability:{...old.provenance.applicability,[field]:older}}; + fresh.provenance={...fresh.provenance,applicability:{...fresh.provenance.applicability,[field]:newer}}; + assert.equal(mergeRanked([[old,fresh]],8)[0].url,old.url); + } +}); + +test('publisher-declared software scope survives ingestion; article language and layout never become product scope', async () => { + const page=await ingest(`
Capture settings.
`); + assert.deepEqual(page.provenance.applicability,{product:'Synthetic Recorder',surface:'Android',version:'2.1'}); + const unspecified=await ingest(`
Capture settings for version 2.
`); + assert.equal(unspecified.provenance.applicability,undefined); + assert.match(formatEvidence(chunkDocument(unspecified)),/applicability incomplete/); +}); + +test('newer complementary instructions and older safety details both survive unchanged evidence limits', async () => { + const [old,fresh]=competingRows(); + old.body='Capture pause has an indicator; keep local captures until upload is confirmed.'; + fresh.body='Capture pause is available from the toolbar.'; + const rows=[old,fresh]; + const evidence=await storedDocs('Explain capture pause.',{searchDocPages:async(_q,_n,sources=[])=>sources.includes('github')?[]:rows}); + for(const row of rows) {assert.ok(evidence.includes(row.url));assert.ok(evidence.includes(row.body));} + assert.ok(evidence.indexOf(fresh.url) { + const {queryAgent,reviewAnswer}=require('../commandcode'); + const {SOURCE_DATE_POLICY}=require('../sourceProvenance'); + const previous=process.env.CMD_API_KEY; + process.env.CMD_API_KEY='synthetic-source-policy-key'; + t.after(()=>{if(previous===undefined)delete process.env.CMD_API_KEY;else process.env.CMD_API_KEY=previous;}); + let calls=0,reviewAttempts=0; + const post=async(_url,body)=>{ + calls++; + const system=body.messages.filter(row=>row.role==='system').map(row=>row.content).join('\n'); + assert.equal(system.split(SOURCE_DATE_POLICY).length-1,1); + if (system.includes('final relevance and grounding gate') && ++reviewAttempts===1) + throw Object.assign(new Error('synthetic retryable review timeout'),{code:'ECONNABORTED'}); + return {data:{choices:[{message:{content:JSON.stringify({final_answer:'Capture pause depends on the documented application version.', + grounded:true,relevant:true,confidence:0.8,escalate:false,sources_used:['S1']})}}]}}; + }; + await queryAgent({question:'Explain capture pause.',route:{lane:'faq'},post}); + await reviewAnswer({question:'Explain capture pause.',draft:'Capture pause depends on the documented version.', + lane:'faq',sources:formatEvidence(competingRows()),post}); + assert.equal(calls,3); + assert.equal(reviewAttempts,2,'existing timeout retry exercises the compact reviewer path'); +}); + +test('two- and three-part live retrieval keeps the six parallel fetches and unchanged per-page timeout', async t => { + const budgets=[]; + t.mock.method(AbortSignal,'timeout',ms=>{budgets.push(ms);return new AbortController().signal;}); + const pages=['Capturealpha','Capturebeta','Capturegamma'].flatMap(term=>Array.from({length:3},(_,i)=>({ + title:`${term} guide ${i}`,url:`https://docs.omi.me/synthetic-${term.toLowerCase()}-${i}.md`, + }))); + for (const count of [2,3]) { + const started=[]; let release; + const waiting=new Promise(resolve=>{release=resolve;}); + const pending=relevantDocs('Capturealpha Capturebeta Capturegamma procedures',{ + queries:['Capturebeta','Capturegamma'], + subQuestions:pages.filter((_,i)=>i%3===0).slice(0,count).map(page=>({question:page.title,query:page.title})), + fetchImpl:async(address,options)=>{ + assert.ok(options.signal); + if(String(address).endsWith('/llms.txt'))return{ok:true,text:async()=>pages.map(page=>`- [${page.title}](${page.url}): Capture procedures`).join('\n')}; + started.push(address); await waiting; + return{ok:true,text:async()=> '# Capture procedure\nUse the documented controls.'}; + }, + }); + await new Promise(resolve=>setImmediate(resolve)); + try {assert.equal(started.length,6,'all six requests start before any page completes');} + finally {release();} + const evidence=await pending; + assert.ok(evidence.length<=9000); + assert.ok((evidence.match(/^\[S\d+ \|/gm)||[]).length<=8); + } + assert.ok(budgets.length>=12); + assert.ok(budgets.every(ms=>ms===10000)); +});