Skip to content

feat: Read-State Awareness — eliminate fabricated continuity language in symbolic moment reads#474

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/add-read-state-awareness
Draft

feat: Read-State Awareness — eliminate fabricated continuity language in symbolic moment reads#474
Copilot wants to merge 3 commits intomainfrom
copilot/add-read-state-awareness

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 30, 2026

Raven was generating **Changed since last check:** No new signal. on every symbolic moment read regardless of whether any prior read existed — fabricating continuity on first reads and ignoring real diffs on continuation reads.

New modules

  • readState.ts — Pure domain layer: ReadState, ReadHistoryEntry, ContinuityDiff types + pure functions computeReadState, computeContinuityDiff, generateContinuityLanguage, validateReadStateConsistency, isFirstReadOrDefault
  • readHistory.ts — Firestore-backed persistence per user/chart: appendRead, getMostRecentRead, getReadsForDate. Errors degrade silently; read path is never blocked

Behavioral changes

Read state Before After
First read ever Changed since last check: No new signal. (fabricated) Block omitted
First read today Changed since last check: No new signal. (fabricated) Block omitted
Continuation same aperture Changed since last check: No new signal. (static) Real orb/Moon/chamber diff language
No readState provided (legacy) Changed since last check: No new signal. Block omitted (defaults first-read)

Wiring

  • route.ts — Before generation: derives stable chartFingerprint from birth data, fetches today's reads + most-recent in parallel, computes ReadState, passes it to buildStructuredSymbolicMomentReply. After successful delivery: appends ReadHistoryEntry to Firestore (fire-and-forget)
  • symbolicMomentComposer.ts — Accepts optional readState; suppresses continuity block when isFirstRead(readState) is true
  • symbolicMomentFrontstage.ts — Threads readState through buildStructuredSymbolicMomentReply; both structural validators gate the continuity-block requirement via isFirstReadOrDefault; SealedSymbolicMomentAudit (Zone 2) includes a read_state snapshot

Validator enforcement

// Blocks fabricated continuity on first reads
validateReadStateConsistency(proseOutput, firstReadState)
// → { passes: false, failureType: 'FABRICATED_CONTINUITY_ON_FIRST_READ' }

// Blocks missing continuity on continuation reads (> 5 min elapsed)
validateReadStateConsistency(proseOutput, continuationState)
// → { passes: false, failureType: 'CONTINUATION_READ_MISSING_CONTINUITY_LANGUAGE' }

Tests

17 new regression tests in readState.test.ts covering all read state scenarios, orb tightening/loosening, Moon sign change detection, and both validator failure modes. Existing symbolicMomentFrontstage.test.ts updated: the test that previously asserted fabricated No new signal. now asserts doesNotMatch(/\*\*Changed since last check:\*\*/). Continuation tests explicitly pass readState: { is_continuation: true, ... } to exercise the correct path. Both files added to the smoke suite.

Copilot AI and others added 3 commits April 30, 2026 21:20
- Add readState.ts with ReadState types, computeReadState, computeContinuityDiff,
  generateContinuityLanguage, and validateReadStateConsistency
- Add readHistory.ts with Firestore-backed appendRead/getMostRecentRead/getReadsForDate
- Update symbolicMomentComposer.ts: suppress "Changed since last check" block on
  first reads (when readState is null or is_first_read_ever/is_first_read_today)
- Update symbolicMomentFrontstage.ts: thread readState through buildStructuredSymbolicMomentReply,
  update validators to accept readState and gate the continuity block requirement,
  extend SealedSymbolicMomentAudit with read_state snapshot
- Wire read state in route.ts: compute readState before generation, append read
  history entry after successful (non-void) symbolic moment delivery
- Add 17 regression tests in readState.test.ts covering all read state scenarios
- Update symbolicMomentFrontstage.test.ts: flip the fabricated "No new signal"
  assertion to correctly verify absence of continuity block on first reads
- Add both new test files to the smoke test suite in package.json

Agent-Logs-Url: https://github.com/DHCross/Shipyard/sessions/765b5c8b-62ad-481b-8d6e-61021c7ef338

Co-authored-by: DHCross <45954119+DHCross@users.noreply.github.com>
- Simplify null check: previousDelta == null instead of explicit triple
- Extract isFirstReadOrDefault helper to eliminate repeated null-check pattern
- Use isFirstReadOrDefault in both structural validators (no more inline
  ternary for null/undefined defaulting)
- Extract ASPECT_LABEL_PATTERN as a named constant in route.ts
- Use crypto.randomUUID() for read_id instead of Date.now() + Math.random()

Agent-Logs-Url: https://github.com/DHCross/Shipyard/sessions/765b5c8b-62ad-481b-8d6e-61021c7ef338

Co-authored-by: DHCross <45954119+DHCross@users.noreply.github.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
shipyard Ready Ready Preview, Comment Apr 30, 2026 10:09pm

Request Review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds read-state awareness to Symbolic Moment reads so first-time reads no longer fabricate “Changed since last check” language, while continuation reads can include real diffs (and persist/read history via Firestore).

Changes:

  • Introduces pure domain logic for read-state + continuity diffs (readState.ts) and Firestore-backed persistence (readHistory.ts).
  • Threads readState through Symbolic Moment composition/validation and records a snapshot in the sealed audit payload.
  • Wires the API route to compute read-state from persisted history and append a read entry post-delivery; updates/extends smoke tests.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
vessel/src/lib/server/readHistory.ts New Firestore persistence helpers for read history entries (append + query).
vessel/src/lib/raven/readState.ts New pure read-state + continuity diff computation and validation helpers.
vessel/src/lib/raven/symbolicMomentFrontstage.ts Threads readState through validators and audit sealing; adjusts continuity-block requirements.
vessel/src/lib/raven/symbolicMomentComposer.ts Makes continuity block nullable and suppressible based on readState.
vessel/src/app/api/raven-chat/route.ts Computes chart fingerprint, loads read history, passes readState, and appends read entries.
vessel/src/lib/raven/tests/symbolicMomentFrontstage.test.ts Updates assertions to reflect first-read continuity suppression and read_state snapshot.
vessel/src/lib/raven/tests/readState.test.ts New regression coverage for read-state/diff/validator scenarios.
vessel/package.json Adds new tests to smoke suite.
vessel/sherlog-velocity/data/self-model.json Regenerated Sherlog model metadata.
vessel/sherlog-velocity/data/gap-history.jsonl Appends latest gap-history record.

Comment on lines +102 to +105
.orderBy('timestamp_utc', 'asc')
.get();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return snap.docs.map((doc: any) => doc.data() as ReadHistoryEntry);
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getReadsForDate() uses where('user_local_date', '==', localDate).orderBy('timestamp_utc'). In Firestore this typically requires a composite index (user_local_date + timestamp_utc); without it the query throws and you silently return [], which will make computeReadState() treat continuations as first reads and suppress continuity indefinitely. Consider adding the required index to repo-managed Firestore indexes, or adjust the query/storage shape to avoid needing a composite index.

Suggested change
.orderBy('timestamp_utc', 'asc')
.get();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return snap.docs.map((doc: any) => doc.data() as ReadHistoryEntry);
.get();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const entries = snap.docs.map((doc: any) => doc.data() as ReadHistoryEntry);
return entries.sort((a, b) => a.timestamp_utc.localeCompare(b.timestamp_utc));

Copilot uses AI. Check for mistakes.

export type AspectChange = {
aspect: AspectSnapshot;
delta: number; // positive = orb narrowed (tightening), negative = orb widened (loosening)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AspectChange.delta is documented as “positive = tightening, negative = loosening”, but computeContinuityDiff() always stores a positive magnitude and uses separate tightening/loosening arrays. Please align the comment/type with the actual representation (or store negative deltas for loosening).

Suggested change
delta: number; // positive = orb narrowed (tightening), negative = orb widened (loosening)
delta: number; // non-negative orb-change magnitude; direction is conveyed by membership in tightening vs loosening

Copilot uses AI. Check for mistakes.
Comment on lines +271 to +283
// Nothing material changed
if (
diff.tightening.length === 0 &&
diff.loosening.length === 0 &&
diff.new_aspects.length === 0 &&
!diff.moon_sign_changed
) {
if (diff.minutes_elapsed < 30) {
return `${elapsed} on, the field is essentially where it was. There isn't new news, just confirmation of the line.`;
}
return `The field is steady — what was here ${elapsed} ago is still here.`;
}

Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateContinuityLanguage()'s “Nothing material changed” early-return ignores departed_aspects and chamber_shifted. As a result, an aspect leaving range (or chamber changing) will incorrectly produce a “field is steady” message, and the later departed_aspects branch becomes unreachable in that scenario. Include departed_aspects.length and chamber_shifted in the early-return condition (or handle them before returning).

Copilot uses AI. Check for mistakes.
Comment on lines +1044 to +1048
if (!firstRead) {
const changeBlock = blocks.find((block) => /^\*\*Changed since last check:\*\*/i.test(block));
if (!changeBlock) {
return { passes: false, failureType: 'STRUCTURAL_TEMPLATE_VIOLATION' };
}
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On first reads the continuity block is supposed to be absent, but when firstRead is true this validator only skips requiring the "Changed since last check" block; it does not fail if the block is present. That means fabricated continuity could still pass validation if it appears in Zone 1. Consider explicitly rejecting changeBlock when firstRead is true (and similarly in validateInternalSymbolicMomentStructure).

Suggested change
if (!firstRead) {
const changeBlock = blocks.find((block) => /^\*\*Changed since last check:\*\*/i.test(block));
if (!changeBlock) {
return { passes: false, failureType: 'STRUCTURAL_TEMPLATE_VIOLATION' };
}
const changeBlock = blocks.find((block) => /^\*\*Changed since last check:\*\*/i.test(block));
if (firstRead) {
if (changeBlock) {
return { passes: false, failureType: 'STRUCTURAL_TEMPLATE_VIOLATION' };
}
} else if (!changeBlock) {
return { passes: false, failureType: 'STRUCTURAL_TEMPLATE_VIOLATION' };

Copilot uses AI. Check for mistakes.
Comment on lines +183 to +184
// Matches "Body1 aspect Body2" driver labels for aspect snapshot extraction.
const ASPECT_LABEL_PATTERN = /^([A-Za-z\s]+)\s+(square|opposition|conjunction|trine|sextile|quincunx|semisquare|semisextile|sesquiquadrate)\s+([A-Za-z\s]+)$/i;
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ASPECT_LABEL_PATTERN here supports a narrower set of aspect keywords than the rest of the symbolic-moment pipeline (e.g. symbolicMomentFrontstage parses conjunct and inconjunct). If astrology.anchor.topDrivers[].label can contain those variants, they’ll be silently dropped from primary_aspects, weakening continuity diffs. Consider reusing the same aspect grammar/constants as the driver parser, or expanding this regex to match the full supported vocabulary.

Suggested change
// Matches "Body1 aspect Body2" driver labels for aspect snapshot extraction.
const ASPECT_LABEL_PATTERN = /^([A-Za-z\s]+)\s+(square|opposition|conjunction|trine|sextile|quincunx|semisquare|semisextile|sesquiquadrate)\s+([A-Za-z\s]+)$/i;
const ASPECT_LABEL_KEYWORDS = [
'square',
'opposition',
'conjunction',
'conjunct',
'trine',
'sextile',
'quincunx',
'inconjunct',
'semisquare',
'semisextile',
'sesquiquadrate',
] as const;
// Matches "Body1 aspect Body2" driver labels for aspect snapshot extraction.
const ASPECT_LABEL_PATTERN = new RegExp(
`^([A-Za-z\\s]+)\\s+(${ASPECT_LABEL_KEYWORDS.join('|')})\\s+([A-Za-z\\s]+)$`,
'i',
);

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +10
* Documents are stored under:
* raven_read_history/{userId_chartId}/{readId}
*
* Indexed queries:
* - Most-recent read for a chart (getMostRecentRead)
* - All reads for a chart on a given local date (getReadsForDate)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file header comment says documents live at raven_read_history/{userId_chartId}/{readId}, but the implementation writes to raven_read_history/{partition}/entries/{docId} (nested entries subcollection). Please update the comment (or the schema) so it matches the actual Firestore layout—this affects debugging and index setup.

Copilot uses AI. Check for mistakes.
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
2 Security Hotspots

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants