Compare commits

..

No commits in common. "architecture/story-context-consolidation" and "onboarding" have entirely different histories.

482 changed files with 8112 additions and 150944 deletions

166
AGENTS.md
View File

@ -1,166 +0,0 @@
# PlotDirector Codex Operations Guide
This repository is the PlotDirector ASP.NET Core application. Use this file as the first stop for operational knowledge that is not obvious from source.
## Environment Architecture
- Main repo path: `/srv/repos/PlotDirector`.
- Web app project: `PlotLine/PlotLine.csproj`.
- Test project: `PlotLine.Tests/PlotLine.Tests.csproj`.
- Solution file: `PlotLine.slnx`.
- Word add-in project: `PlotDirector.WordCompanion/`.
- Development site host files: `/srv/apps/plotdirector-dev/current`.
- Development release archive: `/srv/apps/plotdirector-dev/releases/`.
- Development systemd service: `plotdirector-dev`.
- Service working directory: `/srv/apps/plotdirector-dev/current`.
- Service environment file: `/etc/plotdirector/plotdirector.env`.
- The dev app listens locally on `http://127.0.0.1:5050` behind nginx as `https://dev.plotdirector.com/`.
## Database
- Production-style dev database name: `PlotDirector_Development`.
- Older/local default database name in appsettings may be `PlotLine`; do not assume that is the effective dev database.
- Runtime connection details come from `/etc/plotdirector/plotdirector.env`, key `ConnectionStrings__DefaultConnection`.
- Do not print, copy, or commit connection strings, passwords, API keys, SMTP secrets, Stripe secrets, or OpenAI keys.
- The SQL Server is reachable from the app host using the environment-file connection string. If a one-off SQL runner is needed, read the env file safely without shell-evaluating values:
```bash
conn=""
while IFS= read -r line; do
case "$line" in
ConnectionStrings__DefaultConnection=*) conn="${line#ConnectionStrings__DefaultConnection=}" ;;
esac
done < /etc/plotdirector/plotdirector.env
export PLOT_DIRECTOR_SQL_CONNECTION="$conn"
```
- Do not use `source /etc/plotdirector/plotdirector.env`; unquoted values with spaces or special characters can break the shell and leak confusion into the task.
- Use stored procedures for application database access. Do not add inline SQL in application code.
- Old SQL scripts are immutable. Never edit an existing numbered script to fix a live schema issue; add a new forward-only script.
- Before restarting/deploying during Story Intelligence work, check there are no active runs in `StoryIntelligenceRuns`, `StoryIntelligenceJobs`, or `WholeBookPlotSynthesisRuns`.
- Do not restore, reset, or overwrite `PlotDirector_Development` unless the user explicitly asks and the target backup has been verified. Restore backups only to disposable verification databases first.
## SQL Migration Procedure
- Number new scripts under `PlotLine/Sql/` using the next available prefix, for example `175_...sql`.
- Scripts should be idempotent where practical: guard table/column/index creation with existence checks and use `CREATE OR ALTER PROCEDURE`.
- Apply database changes to dev deliberately and capture the script path in the final response.
- After applying SQL, run a smoke query that confirms the expected tables/procedures/columns exist and that the connection reports `PlotDirector_Development`.
- Permanent delete paths have historically exposed missing cascade/cleanup dependencies. When adding tables that reference projects, books, chapters, scenes, characters, candidates, or runs, design deletion behaviour intentionally.
## Build And Test
- Full test gate:
```bash
dotnet run --project PlotLine.Tests/PlotLine.Tests.csproj
```
- Build gate:
```bash
dotnet build PlotLine.slnx
```
- Publish gate:
```bash
dotnet publish PlotLine/PlotLine.csproj -c Release -o /tmp/plotdirector-dev-publish-<commit>-<timestamp>
```
- Known quirk: running build and tests in parallel can cause static web asset cache file locks, especially around `*.staticwebassets*.json` or `rjsmrazor.dswa.cache.json`. Run build/test serially when verifying.
- Prefer `rg`/`rg --files` for repository searches.
## Running Locally
- From `/srv/repos/PlotDirector`, run:
```bash
dotnet run --project PlotLine/PlotLine.csproj
```
- For local database/API work, provide configuration through environment variables, user secrets, or a safe local appsettings override. Do not rely on secrets committed in appsettings, and do not add new secrets to source.
- The deployed dev service uses `/etc/plotdirector/plotdirector.env`; local terminal runs do not automatically use that file unless you explicitly provide equivalent environment variables.
## Development Deployment
Use the existing physical-directory deployment pattern. `current` is a real directory, not a symlink.
1. Confirm the commit to deploy:
```bash
git status --short --branch
git log -1 --oneline
```
2. Confirm no active Story Intelligence work is running before restart.
3. Publish to a fresh temp folder:
```bash
release="<shortsha>-$(date -u +%Y%m%d%H%M%S)"
publish_dir="/tmp/plotdirector-dev-publish-$release"
release_dir="/srv/apps/plotdirector-dev/releases/$release"
backup_dir="/srv/apps/plotdirector-dev/current-before-$release"
dotnet publish PlotLine/PlotLine.csproj -c Release -o "$publish_dir"
```
4. Install the release:
```bash
sudo mkdir -p "$release_dir"
sudo cp -a "$publish_dir"/. "$release_dir"/
sudo chown -R plotdirector:plotdirector "$release_dir"
sudo cp -a /srv/apps/plotdirector-dev/current "$backup_dir"
sudo chown -R plotdirector:plotdirector "$backup_dir"
sudo rsync -a --delete "$release_dir"/ /srv/apps/plotdirector-dev/current/
sudo chown -R plotdirector:plotdirector /srv/apps/plotdirector-dev/current
sudo systemctl restart plotdirector-dev
```
5. Verify:
```bash
systemctl is-active plotdirector-dev
systemctl status plotdirector-dev --no-pager -n 30
curl -k -L --max-time 20 -o /tmp/pd-dev-home.html -w 'http_code=%{http_code}\nfinal_url=%{url_effective}\nbytes=%{size_download}\n' https://dev.plotdirector.com/
sudo stat -c 'CurrentDll=%n %y %s bytes' /srv/apps/plotdirector-dev/current/PlotLine.dll
```
- A `HEAD` request to the dev site may return `405`; use `GET` with `curl -L` for the real smoke check.
- Keep the release folder and current-before backup named with the deployed short SHA and UTC timestamp for traceability.
## Project Structure
- `PlotLine/Controllers/`: MVC controllers.
- `PlotLine/Views/`: Razor views.
- `PlotLine/Services/`: application services, Story Intelligence workers, OpenAI integration, import/onboarding flow.
- `PlotLine/Data/`: repositories and database access.
- `PlotLine/Models/`: domain and DTO models.
- `PlotLine/ViewModels/`: UI view models.
- `PlotLine/Hubs/`: SignalR hubs.
- `PlotLine/wwwroot/`: static CSS/JS/assets.
- `PlotLine/Sql/`: forward-only SQL migration scripts and stored procedure definitions.
- `PlotLine.Tests/Program.cs`: lightweight integration/regression test harness.
- `docs/` and `Docs/`: project documentation stores; check both before creating new documentation.
## Story Intelligence Conventions
- PlotDirector must work for arbitrary books and authors. Do not add production special cases for Alpha Flame or any other manuscript-specific names, places, vehicles, objects, or relationships.
- Manuscript-specific examples may appear in regression fixtures only, and tests should include unrelated examples so production code cannot pass by recognising one fixture.
- Use generic evidence for significance: recurrence, scene/chapter presence, ownership, narrative prominence, title presence, knowledge significance, and participation in important events.
- Relationship classification must use generic interaction evidence and configured relationship types, not character-specific rules.
- Character/alias resolution should use canonical identities, aliases, and evidence rather than book-specific names.
- Scene manuscript text in `SceneManuscriptSources` is the durable source for canonical scene analysis.
- Core import and Story Intelligence are separate phases. Do not reintroduce the old visualisation/replay workflow as part of production import.
- Whole-book plot synthesis should happen after character resolution and should not add another per-scene AI pass.
- If prompt/source text is too large, fail the specific synthesis job clearly rather than silently truncating manuscript text.
## Operational Safety
- Avoid destructive git commands such as `git reset --hard` or `git checkout --` unless the user explicitly asks.
- The worktree may include user changes. Inspect before editing and do not revert unrelated changes.
- Do not deploy while an import or Story Intelligence analysis is active unless the user explicitly accepts the interruption.
- Do not modify current database state for read-only investigation tasks.
- Do not apply SQL during read-only audit tasks.
- Use disposable verification databases for restore checks and drop them afterwards unless instructed otherwise.
- Keep deployment and SQL notes concise in final responses: commit, SQL script, tests, deployed release, service status, and any residual risks.

View File

@ -1,17 +0,0 @@
# Scene relationship review correction
The existing architecture already has the required canonical foundation:
- `CharacterRelationships` identifies participants, type, permanence, start/end scenes, initial state and reciprocity. Character A -> Character B is directional when reciprocity is false.
- `RelationshipEvents` records scene, state, intensity, awareness and description. Scene Inspector and character history already consume these events; relationship summaries derive current state from history.
- Stored scene results preserve relationship JSON. The persisted review candidate system supplies paging and independent author decisions.
The old Story Intelligence importer grouped every scene by an unordered character pair, promoted the strongest whole-book classification, forced reciprocity and awareness, converted confidence to intensity, and created filtered events only after the user accepted the pair. Review exposed neither event states nor direction. Additionally, substring matching treated distrust as friendly trust.
The corrected flow emits individual scene proposals with factual/state/development kind, scene state, direction, awareness, emotional intensity, development and evidence. Review permits editing these and reversing direction. Meaningful developments share an existing matching relationship track; the default changing-state track is Other, while factual ties use their factual type and permanence. Opposite directions and factual ties remain separate. Multiple developments within a scene retain separate identities. Repeated factual mentions yield only the earliest proposal.
Migration `197_SceneRelationshipDevelopments.sql` adds an optional provenance key to existing events and a transactional acceptance procedure. Retries cannot duplicate or overwrite an accepted event. New relationships have no fabricated initial emotional state. Accepting an earlier scene moves a newly established track's start backwards within the book. Existing relationships and events are preserved; no competing history tables are introduced. The existing event deletion/merge infrastructure remains applicable.
Pending legacy pair cards can be rebuilt with `tools/RefreshRelationshipReviews --apply`. The tool snapshots pending cards before replacement; the stored procedure preserves accepted/ignored decisions, including old pair decisions. Completed reviews remain completed. Old analysis lacks explicit direction/intensity; proposals are marked for direction review, unknown intensity stays empty, and no mutual feeling or awareness is assumed. This conversion uses stored evidence, not another AI pass. New analysis supplies the richer fields directly.
Validation covers scene review and acceptance with unrelated character fixtures, factual deduplication, independent directional identity, confidence/intensity separation, whole-page input validation, numeric chronology, SQL retry behaviour, distinct factual/directional tracks and preserved decisions. SQL regression fixtures roll back all data.

View File

@ -6,8 +6,8 @@ The add-in is deliberately small:
- The manifest adds one ribbon button: **Open PlotDirector Companion**. - The manifest adds one ribbon button: **Open PlotDirector Companion**.
- The task pane opens the PlotDirector-hosted `/word-companion` route. - The task pane opens the PlotDirector-hosted `/word-companion` route.
- The manifest requests `ReadWriteDocument` so the companion can read Word paragraphs and write hidden PlotDirector anchors back into the manuscript after import. - The manifest requests `ReadDocument` so the companion can read Word paragraphs, selection context, and document body content for the manual document-structure scan.
- The companion only writes hidden PlotDirector anchors needed to reconnect manuscript paragraphs to PlotDirector scenes. - The companion does not request document write permissions.
## Development ## Development

View File

@ -22,9 +22,9 @@
<DefaultSettings> <DefaultSettings>
<SourceLocation DefaultValue="{{taskpaneUrl}}"/> <SourceLocation DefaultValue="{{taskpaneUrl}}"/>
</DefaultSettings> </DefaultSettings>
<!-- ReadWriteDocument is required because the companion writes hidden PlotDirector anchors <!-- ReadDocument is required for the companion to read Word paragraphs, selection context,
back into the Word document after a manuscript import. --> and document body content. It does not grant document write access. -->
<Permissions>ReadWriteDocument</Permissions> <Permissions>ReadDocument</Permissions>
<VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0"> <VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
<Hosts> <Hosts>
<Host xsi:type="Document"> <Host xsi:type="Document">

View File

@ -22,9 +22,9 @@
<DefaultSettings> <DefaultSettings>
<SourceLocation DefaultValue="https://plotdirector.com/word-companion"/> <SourceLocation DefaultValue="https://plotdirector.com/word-companion"/>
</DefaultSettings> </DefaultSettings>
<!-- ReadWriteDocument is required because the companion writes hidden PlotDirector anchors <!-- ReadDocument is required for the companion to read Word paragraphs, selection context,
back into the Word document after a manuscript import. --> and document body content. It does not grant document write access. -->
<Permissions>ReadWriteDocument</Permissions> <Permissions>ReadDocument</Permissions>
<VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0"> <VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
<Hosts> <Hosts>
<Host xsi:type="Document"> <Host xsi:type="Document">

View File

@ -1,22 +0,0 @@
# Timeline metric browser regressions
Run from this directory:
```sh
npm ci
npx playwright install chromium
npm test
```
Set `CHROMIUM_PATH` to use an existing Chromium executable. The tests load the production metric-chart initializer, repository CSS, and the same Chart.js version referenced by `_Layout.cshtml`. They use isolated DOM fixtures; no database, login, or running application is required. Keep the pinned Chart.js dependency in step with the layout's CDN version.
Coverage includes all four timeline views and three densities, one and three books, headers consuming different horizontal widths, unequal scene widths, timelines exceeding 30,000 CSS pixels, start/middle/end scrolling, viewport resizing, section collapse/expand, and device pixel ratios 1 and 2. Assertions compare every scene centre with its chart coordinate, and visible rendered point positions, with a tolerance below half a CSS pixel. Tooltips, metric toggles, point navigation, and drag-save scene identity are exercised; saves are mocked.
The chart derives coordinates from rendered scene rectangles. A sticky, viewport-sized canvas lives inside the full-width frame in the existing timeline scroll container. The custom Chart.js linear scale uses CSS pixel positions directly, including axis and frame padding, without scaling scene positions to the chart area. ResizeObserver and the timeline layout event invalidate measurements; scroll events only update the visible chart window.
When scene cards are collapsed after measurement, the last measured geometry remains available. If scene cards are disabled or initially collapsed, the chart retains the standalone ordinal layout until rendered scene geometry becomes available. Analytics and character charts retain their standalone layouts. Raw metric rows remain on their existing CSS grid; they are not part of this chart fix.
## Location drag-and-drop regressions
Run `npm run test:locations` (with `CHROMIUM_PATH` if using an existing Chromium).
This loads the full production site script and Bootstrap confirmation modal in an isolated location-tree fixture. AJAX responses are mocked. It checks parent assignment/reassignment, cancellation, cycle/no-op rejection, repeated dragging after tree replacement, scroll preservation, server-error display, and relationship modal/AJAX saves. Database behavior is covered separately by `PlotLine.Tests/Sql/LocationReparent.sql`, whose fixtures always roll back.

View File

@ -1,106 +0,0 @@
const { chromium } = require('playwright');
const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert/strict');
const repo = path.resolve(__dirname, '../..');
(async () => {
const browser = await chromium.launch({ executablePath: process.env.CHROMIUM_PATH });
try {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', error => errors.push(error.message));
await page.route('https://plotdirector.test/**', route => route.fulfill({ contentType: 'text/html', body: '<html><body></body></html>' }));
await page.goto('https://plotdirector.test/Locations');
const layout = fs.readFileSync(path.join(repo, 'PlotLine/Views/Shared/_Layout.cshtml'), 'utf8');
const modalStart = layout.indexOf(' <div class="modal fade" id="plotlineConfirmModal"');
const modal = layout.slice(modalStart, layout.indexOf(' <script', modalStart));
const rows = [ ['1','Room','',''], ['2','Building','',''], ['3','Cupboard','1','Room'], ['4','Annex','2','Building'] ]
.map(([id,name,parent,parentName]) => `<div style="padding:24px;border:1px solid" draggable="true" data-drag-type="location" data-drag-id="${id}" data-drag-label="${name}" data-parent-location-id="${parent}" data-parent-location-name="${parentName}"><a href="/Locations/Details/${id}">${name}</a></div>`).join('');
await page.setContent(`<select data-location-drag-action><option value="parent">Change parent</option><option value="relationship">Create relationship</option></select>
<p data-location-move-error></p><p data-location-move-status></p>
<form action="/Locations/Reparent" method="post" data-location-reparent-form data-confirm-message="Change parent?" data-confirm-button-text="Move location" data-confirm-button-class="btn-primary">
<input type="hidden" name="projectId" value="1"><input type="hidden" name="locationId"><input type="hidden" name="parentLocationId"><input type="hidden" name="expectedParentLocationId">
</form>
<div class="modal fade" tabindex="-1" data-location-relationship-modal><div class="modal-dialog"><section class="modal-content d-none" data-drag-drop-panel><h2 data-drag-panel-title></h2><span data-drag-panel-kicker></span><p data-drag-panel-summary></p>
<p data-location-relationship-error></p><form action="/Locations/DropRelationship" data-drag-form="location-relationship"><input name="FromLocationID"><input name="ToLocationID"><button type="submit">Save relationship</button></form></section></div></div><div style="height:900px"></div><section data-location-tree>${rows}</section><div style="height:2000px"></div>${modal}`);
await page.addStyleTag({ path: path.join(repo,'PlotLine/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css') });
await page.addScriptTag({ path: path.join(repo,'PlotLine/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js') });
// Load the entire production script: an earlier initialization error can disable dragging.
await page.addScriptTag({ path: path.join(repo,'PlotLine/wwwroot/js/site.js') });
await page.evaluate(() => {
window.savedMoves = [];
window.relationshipSaves = 0;
window.failNextSave = false;
window.fetch = async (url, options) => {
if (window.failNextSave) {
window.failNextSave = false;
return new Response(JSON.stringify({error:'The parent has changed. Review the tree.'}), {status:400});
}
if (url.endsWith('/DropRelationship')) {
window.relationshipSaves++;
return new Response(JSON.stringify({message:'Saved'}));
}
const values = Object.fromEntries(options.body);
window.savedMoves.push(values);
const tree = document.querySelector('[data-location-tree]').cloneNode(true);
const source = tree.querySelector(`[data-drag-id="${values.locationId}"]`);
const target = tree.querySelector(`[data-drag-id="${values.parentLocationId}"]`);
source.dataset.parentLocationId = values.parentLocationId;
source.dataset.parentLocationName = target.dataset.dragLabel;
tree.append(source);
return new Response(tree.outerHTML, {headers:{'Content-Type':'text/html'}});
};
});
const drag = async (source, target) => page.locator(`[data-drag-id="${source}"]`).dragTo(page.locator(`[data-drag-id="${target}"]`));
const confirm = page.locator('#plotlineConfirmModal');
await drag(1,2);
await confirm.waitFor({state:'visible'});
assert.match(await confirm.innerText(), /from top level to “Building”/);
assert.equal(await page.evaluate(() => savedMoves.length),0);
await confirm.locator('[data-bs-dismiss="modal"]').last().click();
await confirm.waitFor({state:'hidden'});
await page.locator('.modal-backdrop').waitFor({state:'hidden'});
assert.equal(await page.evaluate(() => savedMoves.length),0);
await drag(1,2);
const scrollBeforeSave = await page.evaluate(() => window.scrollY);
await confirm.locator('[data-confirm-submit]').click();
await confirm.waitFor({state:'hidden'});
await page.locator('.modal-backdrop').waitFor({state:'hidden'});
await page.waitForFunction(() => document.querySelector('[data-location-move-status]').textContent.includes('moved'));
assert.ok(Math.abs(await page.evaluate(() => window.scrollY) - scrollBeforeSave) < 2, 'AJAX update must preserve scroll');
assert.deepEqual(await page.evaluate(() => savedMoves[0]), {projectId:'1',locationId:'1',parentLocationId:'2',expectedParentLocationId:''});
await drag(4,1);
await confirm.waitFor({state:'visible'});
assert.match(await confirm.innerText(), /from Building to “Room”/);
await confirm.locator('[data-confirm-submit]').click();
await confirm.waitFor({state:'hidden'});
await page.locator('.modal-backdrop').waitFor({state:'hidden'});
assert.equal(await page.evaluate(() => savedMoves[1].expectedParentLocationId),'2');
for (const [source,target] of [[1,1],[1,3],[1,2]]) {
await drag(source,target);
assert.equal(await confirm.isVisible(),false);
assert.equal(await page.evaluate(() => savedMoves.length),2);
}
// Server validation failures leave the existing tree intact and explain the error.
await page.evaluate(() => window.failNextSave = true);
await drag(4,2);
await confirm.locator('[data-confirm-submit]').click();
await confirm.waitFor({state:'hidden'});
await page.locator('.modal-backdrop').waitFor({state:'hidden'});
assert.match(await page.locator('[data-location-move-error]').innerText(), /parent has changed/);
assert.equal(await page.locator('[data-drag-id="4"]').getAttribute('data-parent-location-id'),'1');
await page.selectOption('[data-location-drag-action]','relationship');
await drag(1,2);
await page.locator('[data-drag-drop-panel]').waitFor({state:'visible'});
assert.equal(await page.locator('[name="FromLocationID"]').inputValue(),'1');
assert.equal(await page.locator('[name="ToLocationID"]').inputValue(),'2');
const relationshipScroll = await page.evaluate(() => window.scrollY);
await page.locator('[data-drag-form="location-relationship"] button').click();
await page.locator('[data-location-relationship-modal]').waitFor({state:'hidden'});
await page.locator('.modal-backdrop').waitFor({state:'hidden'});
assert.ok(Math.abs(await page.evaluate(() => window.scrollY) - relationshipScroll) < 2);
assert.equal(await page.evaluate(() => relationshipSaves),1);
assert.deepEqual(errors,[]);
console.log('PASS: drag assignment/reassignment, confirmation, cancellation, invalid targets, AJAX refresh, scroll preservation, server errors, relationship modal/AJAX, full site.js initialization');
} finally { await browser.close(); }
})().catch(error => { console.error(error); process.exitCode = 1; });

View File

@ -1,80 +0,0 @@
{
"name": "Browser",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"chart.js": "4.4.9",
"playwright": "1.58.2"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"dev": true,
"license": "MIT"
},
"node_modules/chart.js": {
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
"integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

View File

@ -1,5 +0,0 @@
{
"private": true,
"scripts": { "test": "node timeline-metric-layout.cjs", "test:locations": "node location-reparent.cjs" },
"devDependencies": { "chart.js": "4.4.9", "playwright": "1.58.2" }
}

View File

@ -1,198 +0,0 @@
// npm install && npx playwright install chromium && npm test
// Optional: CHROMIUM_PATH=/path/to/chrome npm test
const { chromium } = require('playwright');
const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert/strict');
const repo = path.resolve(__dirname, '../..');
const source = fs.readFileSync(path.join(repo, 'PlotLine/wwwroot/js/site.js'), 'utf8');
const start = source.indexOf('(() => {\n const chartRoots =');
const chartScript = source.slice(start, source.indexOf('\n})();', start) + 6);
const css = fs.readFileSync(path.join(repo, 'PlotLine/wwwroot/css/site.css'), 'utf8');
const chartJs = path.join(path.dirname(require.resolve('chart.js')), 'chart.umd.js');
const settle = async page => {
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
await page.waitForTimeout(100);
};
async function fixture(page, bookCount, sceneCount, variedHeaders = false, mode = "visible") {
const scenes = [];
let books = '';
for (let book = 0; book < bookCount; book++) {
let chapters = '';
for (let chapter = 0; chapter < 3; chapter++) {
let cards = '';
for (let n = 0; n < sceneCount; n++) {
const id = scenes.length + 1;
scenes.push({ sceneId: id, sceneNumber: id, chapterLabel: `Chapter ${chapter + 1}`,
sceneTitle: `Scene ${id}`, axisLabel: `S${id}`, bookTitle: `Book ${book + 1}`, timelineUrl: `https://plotdirector.test/Timeline?selectedSceneId=${id}` });
cards += `<a class="timeline-scene-card" data-scene-id="${id}">Scene ${id}</a>`;
}
chapters += `<div class="timeline-chapter" style="--chapter-scene-count:${sceneCount}"><div class="timeline-chapter-title">Chapter ${chapter + 1}</div><div class="timeline-scenes">${cards}</div></div>`;
}
books += `<div class="timeline-book" style="--book-scene-count:${sceneCount * 3}"><div class="timeline-book-title" ${variedHeaders ? `style="width:${137 + book * 89}px"` : ''}>Book ${book + 1}</div><div class="timeline-chapters">${chapters}</div></div>`;
}
const payload = { scenes, metrics: ['Tension', 'Action'].map((metricName, i) => ({ metricName, metricTypeId: i + 1,
points: scenes.map(s => ({ sceneId: s.sceneId, value: 3 + s.sceneId % 5 })) })) };
await page.setContent(`<style>*{box-sizing:border-box}body{margin:0}${css}
.timeline-filtered-root{--timeline-label-width:180px;--timeline-grid-gap:10px}
${variedHeaders ? '.timeline-book{display:flex;width:max-content;gap:23px}.timeline-book-title{flex-shrink:0}' : ''}
</style><div class="timeline-filtered-root density-comfortable zoom-chapter" data-timeline-root>
<section class="timeline-shell"><div data-collapse-body="scene-cards"><div class="timeline-series-row"><div class="timeline-project-title">Project</div><div class="timeline-books">${books}</div></div></div>
<div class="metric-timeline-board" style="--scene-count:${scenes.length}"><div data-collapse-body="metrics"><div class="metric-shape-chart" data-metric-shape-chart data-timeline-metric-editor>
<div data-metric-toggles></div><div data-timeline-metric-edit-toolbar><input name="__RequestVerificationToken" value="test"></div><span data-edit-metric-status></span>
<div class="metric-timeline-row"><div class="metric-lane-label">Metric Shape</div><div class="metric-chart-frame metric-timeline-frame"><canvas></canvas></div></div>
<script type="application/json">${JSON.stringify(payload)}</script></div></div></div></section></div>`);
if (mode !== "visible") {
await page.evaluate(mode => {
const body = document.querySelector('[data-collapse-body="scene-cards"]');
if (mode === "disabled") body.remove();
else body.classList.add('timeline-section-collapsed');
}, mode);
}
await page.addScriptTag({ path: chartJs });
await page.addScriptTag({ content: chartScript });
await settle(page);
}
async function checkAlignment(page, label) {
const result = await page.evaluate(() => {
const chart = Chart.getChart(document.querySelector('canvas'));
const left = chart.canvas.getBoundingClientRect().left;
const points = chart.data.datasets[0].data;
const errors = points.filter(p => p.sceneId).map(p => {
const rect = document.querySelector(`[data-scene-id="${p.sceneId}"]`).getBoundingClientRect();
return Math.abs(left + chart.scales.x.getPixelForValue(p.x) - rect.left - rect.width / 2);
});
const drawnErrors = chart.getDatasetMeta(0).data.flatMap((point, i) => {
if (point.skip || point.x < chart.chartArea.left || point.x > chart.chartArea.right) return [];
const rect = document.querySelector(`[data-scene-id="${points[i].sceneId}"]`).getBoundingClientRect();
return [Math.abs(left + point.x - rect.left - rect.width / 2)];
});
return { max: Math.max(...errors), drawnMax: Math.max(0, ...drawnErrors), bitmap: chart.canvas.width,
limit: document.querySelector('.timeline-shell').clientWidth * devicePixelRatio + 1 };
});
assert.ok(result.max < 0.5, `${label}: scale drift ${JSON.stringify(result)}`);
assert.ok(result.drawnMax < 0.5, `${label}: rendered drift ${JSON.stringify(result)}`);
assert.ok(result.bitmap <= result.limit, `${label}: canvas exceeds viewport`);
}
async function checkInteractions(page) {
await page.evaluate(() => {
const canvas = document.querySelector('canvas');
canvas.scrollIntoView({ block: 'center' });
const shell = document.querySelector('.timeline-shell');
shell.scrollLeft = (shell.scrollWidth - shell.clientWidth) / 2;
});
await settle(page);
const point = await page.evaluate(() => {
const chart = Chart.getChart(document.querySelector('canvas'));
const index = chart.getDatasetMeta(0).data.findIndex(p => !p.skip && p.x > chart.width / 2 && p.x < chart.chartArea.right - 20);
const p = chart.getDatasetMeta(0).data[index];
const rect = chart.canvas.getBoundingClientRect();
return { index, sceneId: chart.data.datasets[0].data[index].sceneId, x: rect.left + p.x, y: rect.top + p.y };
});
assert.ok(point.index > 0, 'exercise a scene well beyond the first');
await page.mouse.move(point.x, point.y);
await settle(page);
assert.ok(await page.evaluate(id => {
const chart = Chart.getChart(document.querySelector('canvas'));
return chart.tooltip.title[0].endsWith(`Scene ${id}`);
}, point.sceneId), 'tooltip must resolve scene identity from dataset index');
await page.evaluate(() => {
const checkboxes = document.querySelectorAll('[data-metric-toggles] input');
checkboxes[1].click();
window.savedMetric = null;
window.fetch = async (_, options) => {
window.savedMetric = JSON.parse(options.body);
return { ok: true, json: async () => ({ success: true, value: window.savedMetric.value }) };
};
});
await page.mouse.move(point.x, point.y);
await page.mouse.down();
await page.mouse.move(point.x, point.y - 25, { steps: 3 });
await page.mouse.up();
await settle(page);
const saved = await page.evaluate(() => window.savedMetric);
assert.equal(saved.sceneId, point.sceneId, 'drag saves the correct scene');
assert.equal(saved.metricId, 1);
await checkAlignment(page, 'after metric editing');
await page.evaluate(() => document.querySelectorAll('[data-metric-toggles] input')[1].click());
assert.equal(await page.evaluate(() => Chart.getChart(document.querySelector('canvas')).isDatasetVisible(1)), true);
const navigation = page.waitForRequest(request => request.url().includes('selectedSceneId='));
await page.route('https://plotdirector.test/**', route => route.abort());
await page.evaluate(index => {
const chart = Chart.getChart(document.querySelector('canvas'));
const p = chart.getDatasetMeta(0).data[index];
const rect = chart.canvas.getBoundingClientRect();
chart.canvas.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: rect.left + p.x, clientY: rect.top + p.y }));
}, point.index);
assert.ok((await navigation).url().endsWith(`selectedSceneId=${point.sceneId}`), 'point click targets its scene');
}
(async () => {
const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROMIUM_PATH });
try {
for (const dpr of [1, 2]) {
const page = await browser.newPage({ viewport: { width: 1100, height: 900 }, deviceScaleFactor: dpr });
const errors = [];
page.on('pageerror', error => errors.push(error.message));
for (const [books, count, headers] of [[1, 12, false], [3, 18, true]]) {
await fixture(page, books, count, headers);
for (const density of ['compact', 'comfortable', 'expanded']) {
for (const zoom of ['book', 'chapter', 'scene-detail', 'scene-overview']) {
await page.evaluate(({ density, zoom }) => {
document.querySelector('[data-timeline-root]').className = `timeline-filtered-root density-${density} zoom-${zoom}`;
}, { density, zoom });
await settle(page);
for (const fraction of [0, 0.5, 1]) {
await page.evaluate(f => {
const shell = document.querySelector('.timeline-shell');
shell.scrollLeft = (shell.scrollWidth - shell.clientWidth) * f;
}, fraction);
await settle(page);
await checkAlignment(page, `${books} books ${density}/${zoom} scroll=${fraction} dpr=${dpr}`);
}
}
}
await page.setViewportSize({ width: 760, height: 900 });
await settle(page);
await checkAlignment(page, 'resize');
// Width changes without an application event must trigger ResizeObserver.
await page.evaluate(() => {
document.querySelectorAll('.timeline-scene-card').forEach((el, i) => el.style.width = `${120 + i % 4 * 73}px`);
document.querySelectorAll('.timeline-chapter,.timeline-book').forEach(el => el.style.width = 'max-content');
});
await settle(page);
await checkAlignment(page, 'unequal scene widths');
for (const section of ['metrics', 'scene-cards']) {
await page.evaluate(section => document.querySelector(`[data-collapse-body="${section}"]`).classList.add('timeline-section-collapsed'), section);
await settle(page);
await page.evaluate(section => {
document.querySelector(`[data-collapse-body="${section}"]`).classList.remove('timeline-section-collapsed');
window.dispatchEvent(new Event('plotline:timeline-layout-changed'));
}, section);
await settle(page);
await checkAlignment(page, `expand ${section}`);
}
await page.setViewportSize({ width: 1100, height: 900 });
}
await fixture(page, 1, 6, false, 'collapsed');
await page.evaluate(() => {
document.querySelector('[data-collapse-body="scene-cards"]').classList.remove('timeline-section-collapsed');
window.dispatchEvent(new Event('plotline:timeline-layout-changed'));
});
await settle(page);
await checkAlignment(page, 'initially collapsed scene cards');
await fixture(page, 1, 6, false, 'disabled');
assert.equal(await page.evaluate(() => Chart.getChart(document.querySelector('canvas')).scales.x.type), 'linear', 'standalone layout remains supported');
await fixture(page, 3, 18, true);
await checkInteractions(page);
assert.deepEqual(errors, []);
await page.close();
}
console.log('PASS: timeline metric geometry and interactions (views/densities, single/multi-book, unequal widths, scrolling, resize, collapse/disabled cards, DPR 1/2, long timelines, tooltips, toggles, click, drag-save)');
} finally { await browser.close(); }
})().catch(error => { console.error(error); process.exitCode = 1; });

View File

@ -1,78 +0,0 @@
using System.Reflection;
using PlotLine.Data;
using PlotLine.Models;
using PlotLine.Services;
using PlotLine.ViewModels;
internal static class CharacterProfileAgeTests
{
private static void Check(bool value, string message) { if (!value) throw new Exception(message); }
public static void ProfileUsesStoryDate()
{
var character = new Character { CharacterID = 1, BirthDate = new DateTime(1965, 9, 1) };
var first = new Scene { SceneID = 900, TimeModeName = "Exact Date", StartDateTime = new DateTime(1980, 5, 7) };
var later = new Scene { SceneID = 2, TimeModeName = "Exact Date", StartDateTime = new DateTime(1982, 12, 24) };
SceneCharacter[] appearances = [new() { CharacterID = 1, SceneID = 2 }, new() { CharacterID = 1, SceneID = 900 }];
var result = CharacterAgeDisplayService.GetProfileAge(character, [first, later], appearances);
Check(result.DisplayText == "14 years, 8 months" && result.ReferenceScene?.SceneID == 900, "Age must use first narrative dated appearance, not current date or minimum scene ID.");
character.AgeReferenceSceneID = 2;
result = CharacterAgeDisplayService.GetProfileAge(character, [first, later], appearances);
Check(result.DisplayText == "17 years, 3 months" && result.Context == "At age reference scene", "Explicit age reference must take precedence.");
character.AgeReferenceSceneID = null;
first.StartDateTime = null;
Check(CharacterAgeDisplayService.GetProfileAge(character, [first, later], appearances).ReferenceScene?.SceneID == 2, "Undated appearance must not invent a reference date.");
later.StartDateTime = null;
result = CharacterAgeDisplayService.GetProfileAge(character, [first, later], appearances);
Check(result.ReferenceScene is null && result.Context.Contains("dated story scene"), "Known DOB without chronology must explain why age is unavailable.");
character.AgeAtSeriesStart = 17;
Check(CharacterAgeDisplayService.GetProfileAge(character, [], []).DisplayText == "About 17", "Approximate fallback must stay available.");
character.BirthDate = null; character.AgeAtSeriesStart = null;
Check(CharacterAgeDisplayService.GetProfileAge(character, [], []).DisplayText == "Unknown", "Missing date/age must stay unknown.");
}
public static void DetailAndSaveUseCanonicalBirthDate()
{
var character = new Character { CharacterID = 1, ProjectID = 1, CharacterName = "Nora", BirthDate = new DateTime(1965, 9, 1), AgeReferenceSceneID = 900 };
var timeline = new TimelineData { Project = new Project { ProjectID = 1 }, Books = [new Book { BookID = 1 }], Chapters = [new Chapter { ChapterID = 1, BookID = 1 }],
Scenes = [new Scene { SceneID = 900, ChapterID = 1, TimeModeName = "Exact Date", StartDateTime = new DateTime(1980, 5, 7) }] };
Character? saved = null;
var repository = Proxy<ICharacterRepository>((method, args) => method switch {
"GetCharacterAsync" => Task.FromResult<Character?>(character),
"ListCharactersAsync" => Task.FromResult<IReadOnlyList<Character>>([character]),
"GetTimelineAsync" => Task.FromResult(((IReadOnlyList<Character>)[character], (IReadOnlyList<SceneCharacter>)[new SceneCharacter { CharacterID = 1, SceneID = 900 }])),
"GetLookupsAsync" => Task.FromResult(new CharacterLookupData()),
"SaveCharacterAsync" => Capture((Character)args![0]!),
_ => Default<ICharacterRepository>(method) });
Task<int> Capture(Character value) { saved = value; return Task.FromResult(1); }
var service = new CharacterService(
Proxy<IProjectRepository>((_, _) => Task.FromResult<Project?>(timeline.Project)),
Proxy<IBookRepository>(), repository, Proxy<IProjectCollaborationRepository>(), Proxy<ISubscriptionService>(),
Proxy<IProjectActivityService>(), Proxy<ICurrentUserService>(), Proxy<IVisualIdentityImageService>(), Proxy<ICharacterEnrichmentService>(),
Proxy<ITimelineRepository>((_, _) => Task.FromResult(timeline)));
var detail = service.GetCharacterDetailAsync(1).GetAwaiter().GetResult()!;
Check(detail.DisplayAge == "14 years, 8 months" && detail.AgeReferenceScene?.SceneID == 900, "Detail runtime must supply canonical chronology to age service.");
Check(detail.Character.BirthDate == character.BirthDate, "Detail must keep authored DOB independently of generated analysis.");
service.SaveCharacterAsync(new CharacterEditViewModel { CharacterID = 1, ProjectID = 1, CharacterName = "Nora", BirthDate = new DateTime(1965, 9, 1) }).GetAwaiter().GetResult();
Check(saved?.BirthDate == new DateTime(1965, 9, 1) && saved.AgeReferenceSceneID == 900, "Date-only save must preserve date and existing reference scene.");
service.SaveCharacterAsync(new CharacterEditViewModel { CharacterID = 1, ProjectID = 1, CharacterName = "Nora", BirthDate = null }).GetAwaiter().GetResult();
Check(saved?.BirthDate is null, "Birth date must remain optional and clearable.");
}
private static T Proxy<T>(Func<string, object?[]?, object?>? handler = null) where T : class
{
var result = DispatchProxy.Create<T, StoryPointProxy>();
((StoryPointProxy)(object)result).Handler = handler ?? ((method, _) => Default<T>(method));
return result;
}
private static object? Default<T>(string name)
{
var type = typeof(T).GetMethods().First(x => x.Name == name).ReturnType;
if (type == typeof(Task)) return Task.CompletedTask;
if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(Task<>)) return type.IsValueType ? Activator.CreateInstance(type) : null;
var valueType = type.GenericTypeArguments[0];
object? value = valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(IReadOnlyList<>)
? Array.CreateInstance(valueType.GenericTypeArguments[0], 0)
: valueType.IsValueType ? Activator.CreateInstance(valueType) : null;
return typeof(Task).GetMethod(nameof(Task.FromResult))!.MakeGenericMethod(valueType).Invoke(null, [value]);
}
}

View File

@ -1,31 +0,0 @@
using Microsoft.Extensions.Logging.Abstractions;
using PlotLine.Services;
using PlotLine.ViewModels;
internal static class EntityMergeTests
{
public static void ConfirmationAndDistinctRecordsAreRequired()
{
// Null dependencies make any repository access before validation fail this test.
var locations = new LocationService(null!, null!, null!, null!, NullLogger<LocationService>.Instance);
var assets = new AssetService(null!, null!, null!, null!, null!, null!);
foreach (var merge in new Func<EntityMergeForm, Task>[]
{
async form => { await locations.MergeAsync(form); },
async form => { await assets.MergeAsync(form); }
})
{
Reject(merge, new EntityMergeForm { SourceID = 1, TargetID = 2 }, "Confirm");
Reject(merge, new EntityMergeForm { SourceID = 1, TargetID = 1, Confirm = true }, "different");
Reject(merge, new EntityMergeForm { SourceID = 1, TargetID = 0, Confirm = true }, "different");
Reject(merge, new EntityMergeForm { SourceID = 0, TargetID = 2, Confirm = true }, "different");
}
}
private static void Reject(Func<EntityMergeForm, Task> merge, EntityMergeForm form, string message)
{
try { merge(form).GetAwaiter().GetResult(); }
catch (InvalidOperationException ex) when (ex.Message.Contains(message, StringComparison.Ordinal)) { return; }
throw new Exception("Invalid merge was not rejected before database access.");
}
}

View File

@ -1,108 +0,0 @@
using PlotLine.Models;
using PlotLine.Data;
using System.Reflection;
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using PlotLine.Services;
using PlotLine.ViewModels;
internal static class LocationSignificanceTests
{
private static void Check(bool condition, string message) { if (!condition) throw new Exception(message); }
private static LocationSignificanceEvidence Evidence(int scene, int chapter, bool setting = false, int eventScore = 0, int context = 0)
=> new(scene, chapter, 1, $"Chapter {chapter}, Scene {scene}", setting, eventScore, context, [], [], []);
private static StoryIntelligenceLocationReviewCandidateViewModel Candidate(string key, int score, string? parent = null, string confidence = "High", string status = "Pending")
=> new() { Key = key, LocationName = key, Category = "Named location", ProposedParentCandidateKey = parent, Confidence = confidence, DecisionStatus = status,
Significance = new() { Score = score, Classification = score >= 40 ? "Significant Location" : "Incidental Location" } };
public static void ConsequenceOutranksFrequency()
{
var hallway = LocationSignificanceService.Assess("Room / interior area", Enumerable.Range(1, 100).Select(i => Evidence(i, i, true, context: 18)));
var observatory = LocationSignificanceService.Assess("Named location", [Evidence(200, 1, true, 50, 18)]);
Check(observatory.Score > hallway.Score && observatory.IsSignificant && !hallway.IsSignificant, "One consequential event must outrank 100 routine setting scenes.");
Check(hallway.Score == 26 && hallway.Classification == "Recurring Location", "Frequency must be capped at eight and cannot alone create a primary setting.");
var duplicates = LocationSignificanceService.Assess("Named location", Enumerable.Repeat(Evidence(200, 1, true, 50, 18), 100));
Check(duplicates.Score == observatory.Score, "Repeated observations in the same scene must not inflate score.");
Check(LocationSignificanceService.Assess("Named location", [Evidence(1, 1)]).Classification == "Incidental Location", "A distinctive name alone is incidental.");
Check(LocationSignificanceService.Assess("Named location", Enumerable.Range(1, 6).Select(i => Evidence(i, i, true, 50, 18))).Classification == "Primary Setting", "Consequential multi-chapter settings should be primary.");
}
public static void SceneContextBelongsToSetting()
{
var parsed = new SceneIntelligenceScene { ScenePurpose = new() { ObservedFunction = "A betrayal and rescue transform the investigation." },
PointOfView = new() { CharacterName = "Leena" },
Metrics = new() { ["tension"] = new() { Score = 9 } },
KnowledgeChanges = [new() { KnowledgeItem = "The captain is the informant" }],
NarrativeArcs = [new() { Title = "The missing expedition", Significance = "high" }] };
var scene = new Scene { SceneID = 7, ChapterID = 3, BookID = 1 };
var site = LocationSignificanceService.Observe(scene, parsed, "Chapter 3, Scene 7", true, true, false, null);
var mention = LocationSignificanceService.Observe(scene, parsed, "Chapter 3, Scene 7", false, false, true, "They mention the pantry.");
Check(site.EventScore == 50 && site.ContextScore > 18 && site.Characters.Contains("Leena") && site.PlotLines.Contains("The missing expedition"), "Reuse structured scene purpose, metrics, knowledge, plot and POV evidence.");
Check(mention.EventScore == 0 && mention.ContextScore == 0 && mention.Characters.Count == 0, "An incidental mention cannot inherit the scene's consequential context.");
var hypothetical = LocationSignificanceService.Observe(scene, parsed, "scene", false, false, true, "They might discover a body at the beach.");
Check(hypothetical.EventScore == 0, "Hypothetical consequential vocabulary must not create an actual event site.");
var routine = new SceneIntelligenceScene { ScenePurpose = new() { ObservedFunction = "The conversation reveals the employee staffing schedule." },
NarrativeArcs = [new() { Title = "Work routines", Significance = "minor" }],
KnowledgeChanges = [new() { KnowledgeItem = "The secretary works on Tuesdays." }] };
var routineScore = LocationSignificanceService.Assess("Named location", [LocationSignificanceService.Observe(scene, routine, "scene", true, true, false, null)]);
Check(!routineScore.IsSignificant, "Ordinary exposition and trivial knowledge must not be mistaken for major revelations.");
var explicitSite = LocationSignificanceService.Observe(scene, parsed, "scene", false, false, true, "The abduction occurred at the lighthouse.");
Check(explicitSite.EventScore == 50, "Location-specific evidence can support a consequential site even when off-stage.");
}
public static void PersistedAnalysisKeepsContextAndParentEvidence()
{
Scene[] scenes = [
new() { SceneID = 1, ChapterID = 1, SceneNumber = 1, POVCharacterName = "Maggie", StructuralSetting = "Maggie's bedroom" },
new() { SceneID = 2, ChapterID = 1, SceneNumber = 2, POVCharacterName = "Beth", StructuralSetting = "Beth's bedroom" },
new() { SceneID = 3, ChapterID = 1, SceneNumber = 3 }
];
var ordinary = new SceneIntelligenceScene { SchemaVersion = "1", Setting = new() { LocationName = "bedroom", GenericRoomType = "bedroom" } };
var major = new SceneIntelligenceScene { SchemaVersion = "1", Setting = new() { LocationName = "library", GenericRoomType = "library", ParentLocationHint = "Westmere House" },
ScenePurpose = new() { ObservedFunction = "A rescue follows the abduction." } };
IReadOnlyList<StoryIntelligenceSavedSceneResult> results = scenes.Select(x => new StoryIntelligenceSavedSceneResult { SceneID = x.SceneID,
ParsedJson = JsonSerializer.Serialize(x.SceneID == 3 ? major : ordinary, new JsonSerializerOptions(JsonSerializerDefaults.Web)) }).ToList();
var runs = Proxy<IStoryIntelligenceResultRepository>((method, _) => method switch {
"GetRunAsync" => Task.FromResult<StoryIntelligenceSavedRun?>(new() { Status = "Completed" }),
"ListSceneResultsAsync" => Task.FromResult(results),
_ => throw new Exception($"Unexpected run operation: {method}") });
var locations = Proxy<ILocationRepository>((method, _) => method == "ListByProjectAsync"
? Task.FromResult<IReadOnlyList<LocationItem>>([]) : throw new Exception($"Unexpected location write/read: {method}"));
var sceneRepository = Proxy<ISceneRepository>((method, _) => method == "ListByChapterAsync"
? Task.FromResult<IReadOnlyList<Scene>>(scenes) : throw new Exception($"Unexpected scene write/read: {method}"));
var importer = new StoryIntelligenceLocationImportService(runs, locations, sceneRepository, null!, NullLogger<StoryIntelligenceLocationImportService>.Instance);
var result = importer.BuildSignificanceAsync(new() { ProjectID = 1, BookID = 1, Items = [new() { RunID = 1, ChapterID = 1 }] }).GetAwaiter().GetResult();
Check(result.Keys.Any(x => x.Contains("maggie", StringComparison.OrdinalIgnoreCase)) && result.Keys.Any(x => x.Contains("beth", StringComparison.OrdinalIgnoreCase)), "Assessment preparation must preserve separate contextual bedrooms.");
Check(result.Any(x => x.Key.Contains("westmere", StringComparison.OrdinalIgnoreCase) && x.Value.IsSignificant), "A house must retain the significance of consequential scenes within it.");
Check(result.Values.SelectMany(x => x.KeyScenes).All(x => x.BookID == 1), "Stored assessment evidence must retain canonical book/scene provenance.");
}
private static T Proxy<T>(Func<string, object?[]?, object?> handler) where T : class
{
var proxy = DispatchProxy.Create<T, StoryPointProxy>();
((StoryPointProxy)(object)proxy).Handler = handler;
return proxy;
}
public static void HierarchyAndUncertainty()
{
var parent = Candidate("Estate", 8);
var high = Candidate("Tower", 90, "Estate");
var low = Candidate("Kitchen", 3, "Estate");
var medium = Candidate("Library", 60, "Estate");
var unrelated = Candidate("Harbour", 70);
var unsure = Candidate("Unresolved room", 0, confidence: "Medium");
var reviewed = Candidate("Reviewed ambiguity", 50, confidence: "Low", status: "Reviewed");
var input = new[] { low, unrelated, medium, unsure, parent, high, reviewed };
var all = LocationSignificanceService.OrderHierarchy(input, "all");
Check(all.Take(4).Select(x => x.Key).SequenceEqual(new[] { "Estate", "Tower", "Library", "Kitchen" }), "Descendants must remain grouped; significant siblings first, significant subtrees first.");
Check(high.HierarchyDepth == 1 && parent.HierarchyDepth == 0, "Hierarchy depth must be retained.");
var significant = LocationSignificanceService.OrderHierarchy(input, "significant");
Check(significant.Contains(parent) && significant.Contains(high) && !significant.Contains(low) && significant.Contains(reviewed), "Significant filter must include ancestors and keep already reviewed evidence accessible.");
var uncertain = LocationSignificanceService.OrderHierarchy(input, "uncertain");
Check(uncertain.Count == 1 && uncertain[0] == unsure, "Only genuine unresolved ambiguity belongs in the uncertainty filter, independently of significance.");
Check(LocationSignificanceService.OrderHierarchy([Candidate("A", 50, "B"), Candidate("B", 0, "A")], "significant").Count == 2, "Legacy cyclic parent hints must terminate and retain all candidates.");
var page = new StoryIntelligenceLocationReviewViewModel { FilteredCount = 52, PendingCandidateCount = 2, PageNumber = 3, PageSize = 25, Candidates = [parent, high] };
Check(page.PageCount == 3 && page.ShowingStart == 51 && page.ShowingEnd == 52 && !page.HasNextPage, "Browse pagination must use filter totals, independently of pending decision counts.");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,93 +0,0 @@
using System.Reflection;
using System.Text.Json;
using Microsoft.Extensions.Logging.Abstractions;
using PlotLine.Data;
using PlotLine.Models;
using PlotLine.Services;
using PlotLine.ViewModels;
internal static class RelationshipDevelopmentTests
{
public static void SceneReviewAndAcceptance() => Run().GetAwaiter().GetResult();
private static async Task Run()
{
foreach (var names in new[] { ("Rob", "Maggie"), ("Iris", "Jon") })
{
var accepted = new List<RelationshipDevelopmentAcceptance>();
var lookups = new CharacterLookupData
{
RelationshipTypes = [new() { RelationshipTypeID=1,TypeName="Other" },new() { RelationshipTypeID=2,TypeName="Sibling" }],
RelationshipStates = [new() { RelationshipStateID=1,StateName="Unknown" },new() { RelationshipStateID=2,StateName="Friendly" },new() { RelationshipStateID=3,StateName="Distrustful" },new() { RelationshipStateID=4,StateName="Romantic" }]
};
var characters = Stub<ICharacterRepository>((m,a) => m switch
{
"ListCharactersAsync" => Task.FromResult<IReadOnlyList<Character>>([new() { CharacterID=1, CharacterName=names.Item1 },new() { CharacterID=2,CharacterName=names.Item2 }]),
"ListAliasesAsync" => Task.FromResult<IReadOnlyList<CharacterAlias>>([]),
"ListRelationshipsByProjectAsync" => Task.FromResult<IReadOnlyList<CharacterRelationship>>([]),
"ListRelationshipEventsBySceneAsync" => Task.FromResult<IReadOnlyList<RelationshipEvent>>([]),
"GetLookupsAsync" => Task.FromResult(lookups),
"AcceptRelationshipDevelopmentAsync" => Save((RelationshipDevelopmentAcceptance)a![0]!),
_ => throw new Exception(m)
});
Task<int> Save(RelationshipDevelopmentAcceptance p) { accepted.Add(p); return Task.FromResult(77); }
SceneIntelligenceRelationship Proposal(string kind,string state,bool mutual,string evidence,int? intensity=null) => new()
{
CharacterA=names.Item1,CharacterB=names.Item2,ProposalKind=kind,RelationshipState=state,
IsReciprocal=mutual,IsKnownToOtherCharacter=false,Intensity=intensity,Confidence=.95m,
Development=evidence,Evidence=evidence,RelationshipSignal="affection",FactualRelationshipType=kind=="Factual"?"Sibling":null
};
var results = new List<StoryIntelligenceSavedSceneResult>
{
Result(30,[Proposal("Development","Romantic",false,"A privately develops feelings for B",3),Proposal("Factual","Unknown",true,"They are siblings")]),
Result(10,[Proposal("Development","Friendly",true,"They help each other escape",6),Proposal("Factual","Unknown",true,"They are siblings")]),
Result(20,[Proposal("Development","Distrustful",false,"A discovers B has lied"),new() { CharacterA=names.Item1,CharacterB=names.Item2,RelationshipSignal="trust",Evidence="They merely appear together with no interaction" }])
};
var runs=Stub<IStoryIntelligenceResultRepository>((m,a)=>m switch
{
"GetRunAsync"=>Task.FromResult<StoryIntelligenceSavedRun?>(new() { Status=StoryIntelligenceRunStatuses.Completed }),
"ListSceneResultsAsync"=>Task.FromResult<IReadOnlyList<StoryIntelligenceSavedSceneResult>>(results),
_=>throw new Exception(m)
});
var scenes=Stub<ISceneRepository>((m,a)=> Task.FromResult<IReadOnlyList<Scene>>([
new() { SceneID=30,SceneNumber=3,ChapterNumber=1 },new() { SceneID=10,SceneNumber=1,ChapterNumber=1 },new() { SceneID=20,SceneNumber=2,ChapterNumber=1 }]));
var pipeline=Stub<IStoryIntelligencePipelineStateService>((m,a)=>Task.FromResult<StoryIntelligenceBookPipelineState?>(null));
var service=new StoryIntelligenceRelationshipImportService(runs,characters,scenes,pipeline,NullLogger<StoryIntelligenceRelationshipImportService>.Instance);
var batch=new OnboardingStoryIntelligenceBatch { ProjectID=1,BookID=2,UserID=3,Items=[new() { RunID=1,ChapterID=1 }] };
var review=await service.BuildReviewAsync(batch);
Check(review.Candidates.Count==4,"Meaningful proposals must stay separate by scene, with co-presence excluded.");
Check(review.Candidates.Select(x=>x.FirstSceneID).SequenceEqual(new int?[]{10,10,20,30}),"Review must follow scene order.");
Check(review.Candidates.Last().IsReciprocal==false && review.Candidates.Last().RelationshipState=="Romantic","One-sided feelings must not become mutual.");
Check(review.Candidates.First(x=>x.ProposalKind=="Factual").RelationshipType=="Sibling","Factual ties must stay separate.");
var form=new StoryIntelligenceRelationshipImportForm { Relationships=review.Candidates.Select(x=>new StoryIntelligenceRelationshipImportChoiceForm
{ Key=x.Key,RelationshipTypeID=x.RelationshipTypeID,RelationshipStateID=x.RelationshipStateID,IsReciprocal=x.IsReciprocal,
IsKnownToOtherCharacter=x.IsKnownToOtherCharacter,Intensity=x.Intensity,Development=x.Development }).ToList() };
form.Relationships.Last().ReverseDirection=true;
var result=await service.ImportAsync(batch,form,false);
Check(result.Success && accepted.Count==4,"Acceptance must save all scene proposals.");
Check(accepted.Last().CharacterAID==2 && accepted.Last().CharacterBID==1 && !accepted.Last().IsReciprocal,"Direction correction must reach persistence.");
Check(accepted.Last().Intensity==3 && accepted.Any(x=>x.Intensity is null),"Confidence must never become intensity.");
Check(accepted.Single(x=>x.IsPermanent).RelationshipTypeID==2,"Only factual ties should be permanent.");
Check(form.Relationships.All(x=>x.AcceptedRelationshipID==77),"Review decisions must reference the canonical relationship ID, not the type ID.");
accepted.Clear();
form.Relationships.Last().RelationshipStateID=999;
Check(!(await service.ImportAsync(batch,form,false)).Success && accepted.Count==0,"Invalid later rows must prevent all writes.");
}
Check(Key(10,1,2,false)!=Key(10,2,1,false),"Directional keys must distinguish reverse feelings.");
Check(Key(10,1,2,true)==Key(10,2,1,true),"Mutual duplicate proposals must share identity.");
Check(Key(10,1,2,false)!=Key(20,1,2,false),"Later scenes must retain independent identity.");
}
private static string Key(int scene,int a,int b,bool reciprocal) => (string)typeof(StoryIntelligenceRelationshipImportService).GetMethod("DevelopmentKey",BindingFlags.NonPublic|BindingFlags.Static)!.Invoke(null,[scene,a,b,reciprocal,"Development","trust","Evidence"])!;
private static StoryIntelligenceSavedSceneResult Result(int scene,List<SceneIntelligenceRelationship> proposals) => new() { SceneID=scene,ParsedJson=JsonSerializer.Serialize(new SceneIntelligenceScene { SchemaVersion="1.0",Relationships=proposals }) };
private static void Check(bool value,string message) { if(!value)throw new Exception(message); }
private static T Stub<T>(Func<string,object?[]?,object?> handler) where T:class
{
var proxy=DispatchProxy.Create<T,RelationshipTestProxy>();
((RelationshipTestProxy)(object)proxy).Handler=handler;
return proxy;
}
}
public class RelationshipTestProxy:DispatchProxy
{
public Func<string,object?[]?,object?> Handler { get; set; }=null!;
protected override object? Invoke(MethodInfo? method,object?[]? args)=>Handler(method!.Name,args);
}

View File

@ -1,243 +0,0 @@
-- Run against a migrated verification/dev database with at least one project and scene.
-- All fixture changes are rolled back, including on assertion failure.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int, @UserID int, @SceneID int;
SELECT TOP (1) @ProjectID = p.ProjectID, @UserID = p.UserID, @SceneID = s.SceneID
FROM dbo.ProjectUserAccess p JOIN dbo.Books b ON b.ProjectID = p.ProjectID
JOIN dbo.Chapters c ON c.BookID = b.BookID JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID
WHERE p.IsActive = 1;
IF @SceneID IS NULL THROW 51001, 'Merge tests require a project with a scene and active user.', 1;
DECLARE @Source int, @Target int, @Child int;
INSERT dbo.Locations(ProjectID, LocationName) VALUES (@ProjectID, N'Merge test old observatory'); SET @Source = SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID, LocationName, Description) VALUES (@ProjectID, N'Merge test observatory', N'Keep target details'); SET @Target = SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID, LocationName, ParentLocationID) VALUES (@ProjectID, N'Merge test dome', @Source); SET @Child = SCOPE_IDENTITY();
INSERT dbo.LocationAliases(LocationID, Alias) VALUES (@Source, N'North tower'), (@Target, N'North tower'), (@Source, N'Old dome');
INSERT dbo.WordCompanionSceneLocations(SceneID, LocationID) VALUES (@SceneID, @Source), (@SceneID, @Target);
INSERT dbo.SceneLocationSuggestions(SceneID, LocationID, CreatedByUserID) VALUES (@SceneID, @Source, @UserID), (@SceneID, @Target, @UserID);
UPDATE dbo.Scenes SET PrimaryLocationID = @Source WHERE SceneID = @SceneID;
DECLARE @RelType int = (SELECT TOP (1) LocationRelationshipTypeID FROM dbo.LocationRelationshipTypes);
INSERT dbo.LocationRelationships(FromLocationID, ToLocationID, LocationRelationshipTypeID)
VALUES (@Source,@Target,@RelType),(@Source,@Child,@RelType),(@Target,@Child,@RelType);
EXEC dbo.Location_Merge @Source, @Target, @UserID;
-- Exercise the post-merge activity write used by the service, not just the merge procedure.
EXEC dbo.ProjectAudit_Record @ProjectID=@ProjectID, @UserID=@UserID,
@ActivityType=N'Merged', @EntityType=N'Location', @EntityID=@Target,
@EntityName=N'Merge regression target', @Description=N'Merge regression activity';
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectActivity WHERE ProjectID=@ProjectID
AND EntityType=N'Location' AND EntityID=@Target AND ActivityType=N'Merged')
THROW 51001, 'Location merge activity was not recorded.', 1;
IF NOT EXISTS (SELECT 1 FROM dbo.Locations WHERE LocationID=@Source AND IsArchived=1) THROW 51001, 'Source location was not archived.', 1;
IF NOT EXISTS (SELECT 1 FROM dbo.Locations WHERE LocationID=@Child AND ParentLocationID=@Target) THROW 51001, 'Child location was not moved.', 1;
IF NOT EXISTS (SELECT 1 FROM dbo.Scenes WHERE SceneID=@SceneID AND PrimaryLocationID=@Target) THROW 51001, 'Primary scene location was not moved.', 1;
IF (SELECT COUNT(*) FROM dbo.LocationAliases WHERE LocationID=@Target) <> 3 THROW 51001, 'Location names were lost or duplicated.', 1;
IF (SELECT COUNT(*) FROM dbo.WordCompanionSceneLocations WHERE SceneID=@SceneID AND LocationID=@Target) <> 1 THROW 51001, 'Duplicate location scene link.', 1;
IF (SELECT COUNT(*) FROM dbo.SceneLocationSuggestions WHERE SceneID=@SceneID AND LocationID=@Target AND Status=N'Pending') <> 1 THROW 51001, 'Duplicate location suggestion.', 1;
IF (SELECT COUNT(*) FROM dbo.LocationRelationships WHERE FromLocationID=@Target AND ToLocationID=@Child AND IsArchived=0) <> 1 THROW 51001, 'Duplicate location relationship.', 1;
IF EXISTS (SELECT 1 FROM dbo.LocationRelationships WHERE (FromLocationID=@Source OR ToLocationID=@Source) AND IsArchived=0) THROW 51001, 'Active relationship left on source.', 1;
IF NOT EXISTS (SELECT 1 FROM dbo.Locations WHERE LocationID=@Target AND Description=N'Keep target details') THROW 51001, 'Target location details changed.', 1;
DECLARE @Kind int = (SELECT TOP (1) AssetKindID FROM dbo.AssetKinds), @Other int;
INSERT dbo.StoryAssets(ProjectID, AssetName, AssetKindID) VALUES (@ProjectID,N'Merge test brass compass',@Kind); SET @Source=SCOPE_IDENTITY();
INSERT dbo.StoryAssets(ProjectID, AssetName, AssetKindID,Description) VALUES (@ProjectID,N'Merge test compass',@Kind,N'Keep target details'); SET @Target=SCOPE_IDENTITY();
INSERT dbo.StoryAssets(ProjectID, AssetName, AssetKindID) VALUES (@ProjectID,N'Merge test map',@Kind); SET @Other=SCOPE_IDENTITY();
INSERT dbo.AssetAliases(StoryAssetID, Alias) VALUES (@Source,N'Navigator'),(@Target,N'Navigator'),(@Source,N'Brass guide');
INSERT dbo.WordCompanionSceneAssets(SceneID,StoryAssetID) VALUES (@SceneID,@Source),(@SceneID,@Target);
INSERT dbo.SceneAssetSuggestions(SceneID,AssetID,CreatedByUserID) VALUES (@SceneID,@Source,@UserID),(@SceneID,@Target,@UserID);
DECLARE @EventType int=(SELECT TOP (1) AssetEventTypeID FROM dbo.AssetEventTypes), @CustodyType int=(SELECT TOP (1) AssetCustodyEventTypeID FROM dbo.AssetCustodyEventTypes);
INSERT dbo.AssetEvents(StoryAssetID,SceneID,AssetEventTypeID,EventTitle) VALUES (@Source,@SceneID,@EventType,N'Merge test event');
DECLARE @EventID int=SCOPE_IDENTITY();
INSERT dbo.AssetCustodyEvents(StoryAssetID,SceneID,AssetCustodyEventTypeID) VALUES (@Source,@SceneID,@CustodyType);
DECLARE @CustodyID int=SCOPE_IDENTITY(), @DepType int=(SELECT TOP (1) AssetDependencyTypeID FROM dbo.AssetDependencyTypes);
INSERT dbo.AssetDependencies(SourceAssetID,TargetAssetID,AssetDependencyTypeID) VALUES (@Source,@Target,@DepType),(@Source,@Other,@DepType),(@Target,@Other,@DepType);
EXEC dbo.StoryAsset_Merge @Source,@Target,@UserID;
-- Exercise the post-merge activity write used by the service, not just the merge procedure.
EXEC dbo.ProjectAudit_Record @ProjectID=@ProjectID, @UserID=@UserID,
@ActivityType=N'Merged', @EntityType=N'Asset', @EntityID=@Target,
@EntityName=N'Merge regression target', @Description=N'Merge regression activity';
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectActivity WHERE ProjectID=@ProjectID
AND EntityType=N'Asset' AND EntityID=@Target AND ActivityType=N'Merged')
THROW 51001, 'Asset merge activity was not recorded.', 1;
IF NOT EXISTS (SELECT 1 FROM dbo.StoryAssets WHERE StoryAssetID=@Source AND IsArchived=1) THROW 51001, 'Source asset was not archived.', 1;
IF (SELECT COUNT(*) FROM dbo.AssetAliases WHERE StoryAssetID=@Target) <> 3 THROW 51001, 'Asset names were lost or duplicated.', 1;
IF (SELECT COUNT(*) FROM dbo.WordCompanionSceneAssets WHERE SceneID=@SceneID AND StoryAssetID=@Target) <> 1 THROW 51001, 'Duplicate asset scene link.', 1;
IF (SELECT COUNT(*) FROM dbo.SceneAssetSuggestions WHERE SceneID=@SceneID AND AssetID=@Target AND Status=N'Pending') <> 1 THROW 51001, 'Duplicate asset suggestion.', 1;
IF NOT EXISTS (SELECT 1 FROM dbo.AssetEvents WHERE AssetEventID=@EventID AND StoryAssetID=@Target) THROW 51001, 'Asset event was not moved.', 1;
IF NOT EXISTS (SELECT 1 FROM dbo.AssetCustodyEvents WHERE AssetCustodyEventID=@CustodyID AND StoryAssetID=@Target) THROW 51001, 'Custody was not moved.', 1;
IF (SELECT COUNT(*) FROM dbo.AssetDependencies WHERE SourceAssetID=@Target AND TargetAssetID=@Other AND IsArchived=0) <> 1 THROW 51001, 'Duplicate asset dependency.', 1;
IF EXISTS (SELECT 1 FROM dbo.AssetDependencies WHERE (SourceAssetID=@Source OR TargetAssetID=@Source) AND IsArchived=0) THROW 51001, 'Active dependency left on source.', 1;
IF NOT EXISTS (SELECT 1 FROM dbo.StoryAssets WHERE StoryAssetID=@Target AND Description=N'Keep target details') THROW 51001, 'Target asset details changed.', 1;
ROLLBACK TRANSACTION;
SELECT N'PASS: location and asset merges; fixtures rolled back' AS Result;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
THROW;
END CATCH;
GO
-- Hierarchy failure must also leave the source unmodified (the procedure rolls back the fixtures).
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int, @UserID int, @Source int, @Target int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Merge cycle test parent'); SET @Source=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName,ParentLocationID) VALUES (@ProjectID,N'Merge cycle test child',@Source); SET @Target=SCOPE_IDENTITY();
EXEC dbo.Location_Merge @Source,@Target,@UserID;
THROW 51001, 'Hierarchy cycle was accepted.', 1;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER() <> 51000 OR ERROR_MESSAGE() NOT LIKE '%descendants%' THROW;
SELECT N'PASS: hierarchy cycle rejected; fixtures rolled back' AS Result;
END CATCH;
GO
-- Reject same record for Location without leaving fixture data behind.
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int, @UserID int, @Source int, @Target int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Merge validation source'); SET @Source=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Merge validation target'); SET @Target=SCOPE_IDENTITY();
SET @Target=@Source;
EXEC dbo.Location_Merge @Source,@Target,@UserID;
THROW 51001, 'Invalid merge was accepted.', 1;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER() <> 51000 THROW;
SELECT N'PASS: Location rejects same record; fixtures rolled back' AS Result;
END CATCH;
GO
-- Reject archived target for Location without leaving fixture data behind.
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int, @UserID int, @Source int, @Target int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Merge validation source'); SET @Source=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Merge validation target'); SET @Target=SCOPE_IDENTITY();
UPDATE dbo.Locations SET IsArchived=1 WHERE LocationID=@Target;
EXEC dbo.Location_Merge @Source,@Target,@UserID;
THROW 51001, 'Invalid merge was accepted.', 1;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER() <> 51000 THROW;
SELECT N'PASS: Location rejects archived target; fixtures rolled back' AS Result;
END CATCH;
GO
-- Reject other project for Location without leaving fixture data behind.
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int, @UserID int, @Source int, @Target int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Merge validation source'); SET @Source=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Merge validation target'); SET @Target=SCOPE_IDENTITY();
INSERT dbo.Projects(ProjectName) VALUES (N'Merge test other project');
DECLARE @OtherProjectID int=SCOPE_IDENTITY();
UPDATE dbo.Locations SET ProjectID=@OtherProjectID WHERE LocationID=@Target;
EXEC dbo.Location_Merge @Source,@Target,@UserID;
THROW 51001, 'Invalid merge was accepted.', 1;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER() <> 51000 THROW;
SELECT N'PASS: Location rejects other project; fixtures rolled back' AS Result;
END CATCH;
GO
-- Reject unauthorized user for Location without leaving fixture data behind.
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int, @UserID int, @Source int, @Target int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Merge validation source'); SET @Source=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Merge validation target'); SET @Target=SCOPE_IDENTITY();
SET @UserID=NULL;
EXEC dbo.Location_Merge @Source,@Target,@UserID;
THROW 51001, 'Invalid merge was accepted.', 1;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER() <> 51000 THROW;
SELECT N'PASS: Location rejects unauthorized user; fixtures rolled back' AS Result;
END CATCH;
GO
-- Reject same record for StoryAsset without leaving fixture data behind.
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int, @UserID int, @Source int, @Target int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.StoryAssets(ProjectID,AssetName,AssetKindID) VALUES (@ProjectID,N'Merge validation source',(SELECT TOP (1) AssetKindID FROM dbo.AssetKinds)); SET @Source=SCOPE_IDENTITY();
INSERT dbo.StoryAssets(ProjectID,AssetName,AssetKindID) VALUES (@ProjectID,N'Merge validation target',(SELECT TOP (1) AssetKindID FROM dbo.AssetKinds)); SET @Target=SCOPE_IDENTITY();
SET @Target=@Source;
EXEC dbo.StoryAsset_Merge @Source,@Target,@UserID;
THROW 51001, 'Invalid merge was accepted.', 1;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER() <> 51000 THROW;
SELECT N'PASS: StoryAsset rejects same record; fixtures rolled back' AS Result;
END CATCH;
GO
-- Reject archived target for StoryAsset without leaving fixture data behind.
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int, @UserID int, @Source int, @Target int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.StoryAssets(ProjectID,AssetName,AssetKindID) VALUES (@ProjectID,N'Merge validation source',(SELECT TOP (1) AssetKindID FROM dbo.AssetKinds)); SET @Source=SCOPE_IDENTITY();
INSERT dbo.StoryAssets(ProjectID,AssetName,AssetKindID) VALUES (@ProjectID,N'Merge validation target',(SELECT TOP (1) AssetKindID FROM dbo.AssetKinds)); SET @Target=SCOPE_IDENTITY();
UPDATE dbo.StoryAssets SET IsArchived=1 WHERE StoryAssetID=@Target;
EXEC dbo.StoryAsset_Merge @Source,@Target,@UserID;
THROW 51001, 'Invalid merge was accepted.', 1;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER() <> 51000 THROW;
SELECT N'PASS: StoryAsset rejects archived target; fixtures rolled back' AS Result;
END CATCH;
GO
-- Reject other project for StoryAsset without leaving fixture data behind.
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int, @UserID int, @Source int, @Target int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.StoryAssets(ProjectID,AssetName,AssetKindID) VALUES (@ProjectID,N'Merge validation source',(SELECT TOP (1) AssetKindID FROM dbo.AssetKinds)); SET @Source=SCOPE_IDENTITY();
INSERT dbo.StoryAssets(ProjectID,AssetName,AssetKindID) VALUES (@ProjectID,N'Merge validation target',(SELECT TOP (1) AssetKindID FROM dbo.AssetKinds)); SET @Target=SCOPE_IDENTITY();
INSERT dbo.Projects(ProjectName) VALUES (N'Merge test other project');
DECLARE @OtherProjectID int=SCOPE_IDENTITY();
UPDATE dbo.StoryAssets SET ProjectID=@OtherProjectID WHERE StoryAssetID=@Target;
EXEC dbo.StoryAsset_Merge @Source,@Target,@UserID;
THROW 51001, 'Invalid merge was accepted.', 1;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER() <> 51000 THROW;
SELECT N'PASS: StoryAsset rejects other project; fixtures rolled back' AS Result;
END CATCH;
GO
-- Reject unauthorized user for StoryAsset without leaving fixture data behind.
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int, @UserID int, @Source int, @Target int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.StoryAssets(ProjectID,AssetName,AssetKindID) VALUES (@ProjectID,N'Merge validation source',(SELECT TOP (1) AssetKindID FROM dbo.AssetKinds)); SET @Source=SCOPE_IDENTITY();
INSERT dbo.StoryAssets(ProjectID,AssetName,AssetKindID) VALUES (@ProjectID,N'Merge validation target',(SELECT TOP (1) AssetKindID FROM dbo.AssetKinds)); SET @Target=SCOPE_IDENTITY();
SET @UserID=NULL;
EXEC dbo.StoryAsset_Merge @Source,@Target,@UserID;
THROW 51001, 'Invalid merge was accepted.', 1;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER() <> 51000 THROW;
SELECT N'PASS: StoryAsset rejects unauthorized user; fixtures rolled back' AS Result;
END CATCH;

View File

@ -1,160 +0,0 @@
-- Regression fixtures are always rolled back. Requires a migrated database and active project user.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int,@UserID int,@Source int,@Parent int,@Child int,@Other int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.Locations(ProjectID,LocationName,Description) VALUES (@ProjectID,N'Reparent test room',N'Preserve details'); SET @Source=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test building'); SET @Parent=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName,ParentLocationID) VALUES (@ProjectID,N'Reparent test cupboard',@Source); SET @Child=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test other building'); SET @Other=SCOPE_IDENTITY();
INSERT dbo.LocationAliases(LocationID,Alias) VALUES (@Source,N'Room alias');
EXEC dbo.Location_Reparent @ProjectID,@Source,@Parent,NULL,@UserID;
IF NOT EXISTS (SELECT 1 FROM dbo.Locations WHERE LocationID=@Source AND ParentLocationID=@Parent) THROW 51001,'Initial parent not assigned.',1;
EXEC dbo.Location_Reparent @ProjectID,@Source,@Other,@Parent,@UserID;
IF NOT EXISTS (SELECT 1 FROM dbo.Locations WHERE LocationID=@Source AND ParentLocationID=@Other AND Description=N'Preserve details') THROW 51001,'Reassignment changed details or failed.',1;
IF NOT EXISTS (SELECT 1 FROM dbo.Locations WHERE LocationID=@Child AND ParentLocationID=@Source) THROW 51001,'Subtree changed.',1;
IF NOT EXISTS (SELECT 1 FROM dbo.LocationAliases WHERE LocationID=@Source AND Alias=N'Room alias') THROW 51001,'Alias changed.',1;
IF (SELECT COUNT(*) FROM dbo.ProjectActivity WHERE EntityType=N'Location' AND EntityID=@Source AND ActivityType=N'Updated')<>2 THROW 51001,'Audit entries missing.',1;
ROLLBACK TRANSACTION;
SELECT N'PASS: assign, reassign, preserve subtree/details/aliases, audit' AS Result;
END TRY
BEGIN CATCH
IF XACT_STATE()<>0 ROLLBACK TRANSACTION;
THROW;
END CATCH;
GO
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int,@UserID int,@Source int,@Parent int,@Child int,@Other int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.Locations(ProjectID,LocationName,Description) VALUES (@ProjectID,N'Reparent test room',N'Preserve details'); SET @Source=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test building'); SET @Parent=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName,ParentLocationID) VALUES (@ProjectID,N'Reparent test cupboard',@Source); SET @Child=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test other building'); SET @Other=SCOPE_IDENTITY();
INSERT dbo.LocationAliases(LocationID,Alias) VALUES (@Source,N'Room alias');
EXEC dbo.Location_Reparent @ProjectID,@Source,@Source,NULL,@UserID;
THROW 51001,'Invalid move was accepted.',1;
END TRY
BEGIN CATCH
IF XACT_STATE()<>0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER()<>51000 THROW;
SELECT N'PASS: reject self; fixtures rolled back' AS Result;
END CATCH;
GO
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int,@UserID int,@Source int,@Parent int,@Child int,@Other int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.Locations(ProjectID,LocationName,Description) VALUES (@ProjectID,N'Reparent test room',N'Preserve details'); SET @Source=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test building'); SET @Parent=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName,ParentLocationID) VALUES (@ProjectID,N'Reparent test cupboard',@Source); SET @Child=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test other building'); SET @Other=SCOPE_IDENTITY();
INSERT dbo.LocationAliases(LocationID,Alias) VALUES (@Source,N'Room alias');
EXEC dbo.Location_Reparent @ProjectID,@Source,@Child,NULL,@UserID;
THROW 51001,'Invalid move was accepted.',1;
END TRY
BEGIN CATCH
IF XACT_STATE()<>0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER()<>51000 THROW;
SELECT N'PASS: reject descendant; fixtures rolled back' AS Result;
END CATCH;
GO
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int,@UserID int,@Source int,@Parent int,@Child int,@Other int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.Locations(ProjectID,LocationName,Description) VALUES (@ProjectID,N'Reparent test room',N'Preserve details'); SET @Source=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test building'); SET @Parent=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName,ParentLocationID) VALUES (@ProjectID,N'Reparent test cupboard',@Source); SET @Child=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test other building'); SET @Other=SCOPE_IDENTITY();
INSERT dbo.LocationAliases(LocationID,Alias) VALUES (@Source,N'Room alias');
EXEC dbo.Location_Reparent @ProjectID,@Source,@Parent,@Other,@UserID;
THROW 51001,'Invalid move was accepted.',1;
END TRY
BEGIN CATCH
IF XACT_STATE()<>0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER()<>51000 THROW;
SELECT N'PASS: reject stale parent; fixtures rolled back' AS Result;
END CATCH;
GO
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int,@UserID int,@Source int,@Parent int,@Child int,@Other int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.Locations(ProjectID,LocationName,Description) VALUES (@ProjectID,N'Reparent test room',N'Preserve details'); SET @Source=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test building'); SET @Parent=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName,ParentLocationID) VALUES (@ProjectID,N'Reparent test cupboard',@Source); SET @Child=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test other building'); SET @Other=SCOPE_IDENTITY();
INSERT dbo.LocationAliases(LocationID,Alias) VALUES (@Source,N'Room alias');
UPDATE dbo.Locations SET ParentLocationID=@Parent WHERE LocationID=@Source;
EXEC dbo.Location_Reparent @ProjectID,@Source,@Parent,@Parent,@UserID;
THROW 51001,'Invalid move was accepted.',1;
END TRY
BEGIN CATCH
IF XACT_STATE()<>0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER()<>51000 THROW;
SELECT N'PASS: reject same parent; fixtures rolled back' AS Result;
END CATCH;
GO
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int,@UserID int,@Source int,@Parent int,@Child int,@Other int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.Locations(ProjectID,LocationName,Description) VALUES (@ProjectID,N'Reparent test room',N'Preserve details'); SET @Source=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test building'); SET @Parent=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName,ParentLocationID) VALUES (@ProjectID,N'Reparent test cupboard',@Source); SET @Child=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test other building'); SET @Other=SCOPE_IDENTITY();
INSERT dbo.LocationAliases(LocationID,Alias) VALUES (@Source,N'Room alias');
UPDATE dbo.Locations SET IsArchived=1 WHERE LocationID=@Parent;
EXEC dbo.Location_Reparent @ProjectID,@Source,@Parent,NULL,@UserID;
THROW 51001,'Invalid move was accepted.',1;
END TRY
BEGIN CATCH
IF XACT_STATE()<>0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER()<>51000 THROW;
SELECT N'PASS: reject archived parent; fixtures rolled back' AS Result;
END CATCH;
GO
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int,@UserID int,@Source int,@Parent int,@Child int,@Other int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.Locations(ProjectID,LocationName,Description) VALUES (@ProjectID,N'Reparent test room',N'Preserve details'); SET @Source=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test building'); SET @Parent=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName,ParentLocationID) VALUES (@ProjectID,N'Reparent test cupboard',@Source); SET @Child=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test other building'); SET @Other=SCOPE_IDENTITY();
INSERT dbo.LocationAliases(LocationID,Alias) VALUES (@Source,N'Room alias');
INSERT dbo.Projects(ProjectName) VALUES (N'Reparent other project'); DECLARE @OtherProject int=SCOPE_IDENTITY(); UPDATE dbo.Locations SET ProjectID=@OtherProject WHERE LocationID=@Parent;
EXEC dbo.Location_Reparent @ProjectID,@Source,@Parent,NULL,@UserID;
THROW 51001,'Invalid move was accepted.',1;
END TRY
BEGIN CATCH
IF XACT_STATE()<>0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER()<>51000 THROW;
SELECT N'PASS: reject cross-project; fixtures rolled back' AS Result;
END CATCH;
GO
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int,@UserID int,@Source int,@Parent int,@Child int,@Other int;
SELECT TOP (1) @ProjectID=ProjectID,@UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
INSERT dbo.Locations(ProjectID,LocationName,Description) VALUES (@ProjectID,N'Reparent test room',N'Preserve details'); SET @Source=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test building'); SET @Parent=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName,ParentLocationID) VALUES (@ProjectID,N'Reparent test cupboard',@Source); SET @Child=SCOPE_IDENTITY();
INSERT dbo.Locations(ProjectID,LocationName) VALUES (@ProjectID,N'Reparent test other building'); SET @Other=SCOPE_IDENTITY();
INSERT dbo.LocationAliases(LocationID,Alias) VALUES (@Source,N'Room alias');
SET @UserID=NULL;
EXEC dbo.Location_Reparent @ProjectID,@Source,@Parent,NULL,@UserID;
THROW 51001,'Invalid move was accepted.',1;
END TRY
BEGIN CATCH
IF XACT_STATE()<>0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER()<>51000 THROW;
SELECT N'PASS: reject unauthorized; fixtures rolled back' AS Result;
END CATCH;
GO

View File

@ -1,38 +0,0 @@
-- Exercise the same audit procedure used by the merge services; leave no data behind.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int, @UserID int;
SELECT TOP (1) @ProjectID=ProjectID, @UserID=UserID FROM dbo.ProjectUserAccess WHERE IsActive=1;
IF @ProjectID IS NULL THROW 51001, 'Audit tests require a project with an active user.', 1;
EXEC dbo.ProjectAudit_Record @ProjectID=@ProjectID, @UserID=@UserID,
@ActivityType=N'Merged', @EntityType=N'Character', @EntityName=N'Merge audit regression';
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectActivity WHERE ProjectID=@ProjectID
AND ActivityType=N'Merged' AND EntityType=N'Character' AND EntityName=N'Merge audit regression')
THROW 51001, 'Character merge activity was not recorded.', 1;
-- Preserve all activity types that were allowed before the merge migration.
INSERT dbo.ProjectActivity(ProjectID,UserID,ActivityType,EntityType,EntityName)
SELECT @ProjectID,@UserID,ActivityType,N'Project',N'Merge audit regression'
FROM (VALUES (N'Created'),(N'Updated'),(N'Deleted'),(N'Archived'),(N'Restored'),
(N'Shared'),(N'CollaboratorRemoved'),(N'Exported')) types(ActivityType);
ROLLBACK TRANSACTION;
SELECT N'PASS: character merge and all existing activity types; fixtures rolled back' AS Result;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
THROW;
END CATCH;
GO
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @ProjectID int=(SELECT TOP (1) ProjectID FROM dbo.Projects);
EXEC dbo.ProjectAudit_Record @ProjectID=@ProjectID, @ActivityType=N'NotAnActivityType',
@EntityType=N'Project', @EntityName=N'Merge audit regression';
THROW 51001, 'Unknown activity type was accepted.', 1;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
IF ERROR_NUMBER() <> 547 OR ERROR_MESSAGE() NOT LIKE N'%CK_ProjectActivity_ActivityType%' THROW;
SELECT N'PASS: unknown activity types remain rejected; fixtures rolled back' AS Result;
END CATCH;
GO

View File

@ -1,44 +0,0 @@
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
INSERT dbo.Projects(ProjectName) VALUES(N'Relationship development regression');
DECLARE @Project int=SCOPE_IDENTITY();
INSERT dbo.Books(ProjectID,BookTitle,BookNumber,SortOrder) VALUES(@Project,N'Regression',1,1);
DECLARE @Book int=SCOPE_IDENTITY(),@Revision int=(SELECT TOP(1) RevisionStatusID FROM dbo.RevisionStatuses),
@TimeMode int=(SELECT TOP(1) TimeModeID FROM dbo.TimeModes),@TimeConfidence int=(SELECT TOP(1) TimeConfidenceID FROM dbo.TimeConfidences);
INSERT dbo.Chapters(BookID,ChapterNumber,ChapterTitle,SortOrder,RevisionStatusID) VALUES(@Book,1,N'Chapter',1,@Revision);
DECLARE @Chapter int=SCOPE_IDENTITY();
INSERT dbo.Scenes(ChapterID,SceneNumber,SceneTitle,SortOrder,TimeModeID,TimeConfidenceID,RevisionStatusID)
VALUES(@Chapter,1,N'Early',1,@TimeMode,@TimeConfidence,@Revision);
DECLARE @Early int=SCOPE_IDENTITY();
INSERT dbo.Scenes(ChapterID,SceneNumber,SceneTitle,SortOrder,TimeModeID,TimeConfidenceID,RevisionStatusID)
VALUES(@Chapter,2,N'Later',2,@TimeMode,@TimeConfidence,@Revision);
DECLARE @Later int=SCOPE_IDENTITY();
INSERT dbo.Characters(ProjectID,CharacterName) VALUES(@Project,N'Iris');
DECLARE @A int=SCOPE_IDENTITY();
INSERT dbo.Characters(ProjectID,CharacterName) VALUES(@Project,N'Jon');
DECLARE @B int=SCOPE_IDENTITY();
DECLARE @Other int=(SELECT RelationshipTypeID FROM dbo.RelationshipTypes WHERE TypeName=N'Other'),
@Sibling int=(SELECT RelationshipTypeID FROM dbo.RelationshipTypes WHERE TypeName=N'Sibling'),
@Friendly int=(SELECT RelationshipStateID FROM dbo.RelationshipStates WHERE StateName=N'Friendly'),
@Distrust int=(SELECT RelationshipStateID FROM dbo.RelationshipStates WHERE StateName=N'Distrustful');
EXEC dbo.StoryIntelligenceRelationship_AcceptDevelopment @Project,@Book,@Later,N'regression:later',@A,@B,@Other,@Distrust,0,0,0,NULL,N'Iris discovers the deception.';
EXEC dbo.StoryIntelligenceRelationship_AcceptDevelopment @Project,@Book,@Early,N'regression:early',@A,@B,@Other,@Friendly,0,0,0,3,N'Iris confides in Jon.';
-- Repeated acceptance must not duplicate or overwrite the original event.
EXEC dbo.StoryIntelligenceRelationship_AcceptDevelopment @Project,@Book,@Early,N'regression:early',@A,@B,@Other,@Distrust,0,0,1,9,N'Changed retry';
IF (SELECT COUNT(*) FROM dbo.CharacterRelationships WHERE ProjectID=@Project)<>1 THROW 51001,'Scene states were split into competing tracks.',1;
IF (SELECT COUNT(*) FROM dbo.RelationshipEvents WHERE SceneID IN(@Early,@Later))<>2 THROW 51001,'Retry duplicated an event.',1;
IF NOT EXISTS(SELECT 1 FROM dbo.CharacterRelationships WHERE ProjectID=@Project AND StartSceneID=@Early AND InitialRelationshipStateID IS NULL) THROW 51001,'Earlier acceptance must move start without projecting a later state backwards.',1;
IF NOT EXISTS(SELECT 1 FROM dbo.RelationshipEvents WHERE SceneID=@Early AND Intensity=3 AND IsKnownToOtherCharacter=0 AND RelationshipStateID=@Friendly) THROW 51001,'Retry overwrote accepted evidence.',1;
EXEC dbo.StoryIntelligenceRelationship_AcceptDevelopment @Project,@Book,@Early,N'regression:reverse',@B,@A,@Other,@Distrust,0,0,0,NULL,N'Jon distrusts Iris.';
EXEC dbo.StoryIntelligenceRelationship_AcceptDevelopment @Project,@Book,@Early,N'regression:factual',@A,@B,@Sibling,@Friendly,1,1,1,NULL,N'They reveal they are siblings.';
IF (SELECT COUNT(*) FROM dbo.CharacterRelationships WHERE ProjectID=@Project)<>3 THROW 51001,'Reverse and factual relationships must remain separate.',1;
IF NOT EXISTS(SELECT 1 FROM dbo.RelationshipEvents e JOIN dbo.CharacterRelationships r ON r.CharacterRelationshipID=e.CharacterRelationshipID
WHERE r.ProjectID=@Project AND r.CharacterAID=@B AND r.CharacterBID=@A AND r.IsReciprocal=0 AND e.RelationshipStateID=@Distrust) THROW 51001,'Reverse state lost.',1;
ROLLBACK TRANSACTION;
SELECT N'PASS: relationship acceptance, retry, direction, factual ties, chronology and null intensity; rolled back' Result;
END TRY
BEGIN CATCH
IF XACT_STATE()<>0 ROLLBACK TRANSACTION;
THROW;
END CATCH;

View File

@ -1,41 +0,0 @@
-- Deliberately scrambled ranks, chapter 2 vs 10, and scene 2 vs 10; all data rolls back.
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
INSERT dbo.Projects(ProjectName) VALUES (N'Relationship ordering fixture');
DECLARE @ProjectID int=SCOPE_IDENTITY();
INSERT dbo.Books(ProjectID,BookTitle,BookNumber,SortOrder) VALUES (@ProjectID,N'Ordering fixture',1,1);
DECLARE @BookID int=SCOPE_IDENTITY();
INSERT dbo.StoryIntelligenceReviewCandidateGenerations(ProjectID,BookID,Module,Status,CandidateCount,PendingCount)
VALUES (@ProjectID,@BookID,N'Relationships',N'Ready',4,3);
DECLARE @GenerationID int=SCOPE_IDENTITY();
INSERT dbo.StoryIntelligenceReviewCandidates(ReviewCandidateGenerationID,ProjectID,BookID,Module,StableKey,DisplayName,SortOrder,CandidatePayloadJson,DecisionStatus)
SELECT @GenerationID,@ProjectID,@BookID,N'Relationships',k,k,rank,payload,status
FROM (VALUES
(N'late',1,N'{"key":"late","firstChapterNumber":10,"firstSceneNumber":1,"firstSceneID":30}',N'Pending'),
(N'middle',2,N'{"key":"middle","firstChapterNumber":2,"firstSceneNumber":10,"firstSceneID":20}',N'Pending'),
(N'early',3,N'{"key":"early","firstChapterNumber":2,"firstSceneNumber":2,"firstSceneID":10}',N'Pending'),
(N'decided',4,N'{"key":"decided","relationshipType":"Friend"}',N'Reviewed')) data(k,rank,payload,status);
CREATE TABLE #Page (
ReviewCandidateID int,ReviewCandidateGenerationID int,ProjectID int,BookID int,Module nvarchar(40),StableKey nvarchar(450),
CandidateType nvarchar(80),DisplayName nvarchar(450),SortOrder int,Confidence decimal(9,4),ExistingCanonicalID int,
DecisionStatus nvarchar(30),DecisionAction nvarchar(40),DecisionTargetKey nvarchar(450),DecisionCanonicalID int,
CandidatePayloadJson nvarchar(max),CreatedUtc datetime2,ReviewedUtc datetime2,UpdatedUtc datetime2,
TotalCount int,PendingCount int,RowNumber bigint);
INSERT #Page EXEC dbo.StoryIntelligenceReviewCandidate_ListPage @BookID,N'Relationships',1,2;
IF NOT EXISTS(SELECT 1 FROM #Page WHERE RowNumber=1 AND StableKey=N'early') THROW 51001,'First page must begin at chapter 2 scene 2.',1;
IF NOT EXISTS(SELECT 1 FROM #Page WHERE RowNumber=2 AND StableKey=N'middle') THROW 51001,'Scene 10 must follow scene 2.',1;
TRUNCATE TABLE #Page;
INSERT #Page EXEC dbo.StoryIntelligenceReviewCandidate_ListPage @BookID,N'Relationships',2,2;
IF NOT EXISTS(SELECT 1 FROM #Page WHERE RowNumber=3 AND StableKey=N'late') THROW 51001,'Chapter 10 must follow chapter 2 across pages.',1;
EXEC dbo.StoryIntelligenceReviewCandidate_RefreshPendingRelationships @BookID,@GenerationID,
N'[{"key":"early","relationshipType":"Other","reviewRulesVersion":2,"firstChapterNumber":2,"firstSceneNumber":2},{"key":"decided","relationshipType":"Other","reviewRulesVersion":2}]';
IF NOT EXISTS(SELECT 1 FROM dbo.StoryIntelligenceReviewCandidates WHERE ReviewCandidateGenerationID=@GenerationID AND StableKey=N'early' AND CandidateType=N'Other') THROW 51001,'Pending suggestion did not refresh.',1;
IF NOT EXISTS(SELECT 1 FROM dbo.StoryIntelligenceReviewCandidates WHERE ReviewCandidateGenerationID=@GenerationID AND StableKey=N'decided' AND JSON_VALUE(CandidatePayloadJson,'$.relationshipType')=N'Friend') THROW 51001,'Author decision changed.',1;
ROLLBACK TRANSACTION;
SELECT N'PASS: numeric chapter/scene paging and preservation of author decisions; rolled back' AS Result;
END TRY
BEGIN CATCH
IF XACT_STATE()<>0 ROLLBACK TRANSACTION;
THROW;
END CATCH;

View File

@ -1,169 +0,0 @@
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PlotLine.Controllers;
using PlotLine.Data;
using PlotLine.Models;
using PlotLine.Services;
using PlotLine.ViewModels;
internal static class StoryPointTests
{
private static void Check(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); }
private static StoryPointSnapshot Snapshot(StoryPointInputs? input = null, int scene = 20)
=> StoryPointContextService.Compose(input ?? Fixture(), scene) ?? throw new Exception("Missing snapshot");
public static void PeopleAndChronology()
{
var point = Snapshot();
Check(point.PovName == "Ari", "POV must resolve the timeline ID from the canonical character batch even when Timeline omits its display name.");
Check(point.Characters.Select(x => x.Appearance.CharacterID).SequenceEqual([1, 2]), "Only selected SceneCharacters belong in presence.");
Check(point.Characters[0].PreviousAppearance?.SceneID == 900, "Previous appearance must cross books and exclude selected scene, regardless of IDs.");
Check(point.Characters[1].PreviousAppearance is null, "First appearance has no previous source.");
Check(point.Characters[0].Age.DisplayText == CharacterAgeDisplayService.GetAgeForScene(Fixture().Characters[0], "Exact Date", new DateTime(2001, 2, 5)).DisplayText,
"Age must use existing service and selected date.");
Check(point.Characters[0].Age.DisplayText == "20 years, 1 month", "Exact age is incorrect.");
var input = Fixture(); input.Timeline.Scenes.Single(x => x.SceneID == 20).StartDateTime = null;
Check(Snapshot(input).Characters[0].Age.DisplayText == "About 19", "Undated chronology must use authoritative approximate age.");
Check(Snapshot(input).Characters[1].Age.DisplayText is null, "Unknown age must stay unknown.");
Check(point.PreviousLocationUse?.SceneID == 900, "Previous location use must cross book boundaries.");
}
public static void KnowledgeBoundary()
{
var point = Snapshot();
Check(point.Characters[0].Knowledge.Single().Description == "The gate code", "Prior knowledge must be retained.");
Check(point.Characters[0].Knowledge.Single().KnowledgeStateName == "Knows", "Later knowledge regression must not change earlier state.");
Check(point.Characters[1].Knowledge.Count == 0, "Future knowledge must not leak.");
var later = Snapshot(scene: 1);
Check(later.Characters.Single(x => x.Appearance.CharacterID == 1).Knowledge.Any(x => x.KnowledgeStateName == "Unaware"), "Later knowledge transition must be reflected.");
Check(point.Sources.Keys.ToHashSet().SetEquals([900, 20]), "Snapshot sources must exclude future scenes.");
}
public static void RelationshipsAndAssets()
{
var point = Snapshot();
Check(point.Relationships.Count == 1 && point.Relationships[0].CurrentStateName == "Trusted", "Earlier relationship event must win; future/undated relationships excluded.");
Check(point.Relationships[0].History.All(x => x.SceneID != 1), "Future relationship history must not leak.");
var asset = point.Assets.Single();
Check(asset.Holder == "Ari" && asset.Location == "Harbour" && asset.Status == "Sealed", "Custody, location and status must be independently historical.");
Check(asset.LastConfirmedSceneID == 20, "Latest asset event must stop at selected scene.");
Check(point.Location!.AssetsPresent.Contains("Case"), "Custody must not erase known asset location.");
var initialInput = Fixture();
var undated = initialInput.Relationships.Relationships.Single(x => x.CharacterRelationshipID == 3);
undated.IsInitialRelationship = true; undated.StartSceneID = 1;
Check(Snapshot(initialInput).Relationships.Count == 1, "Future start must override initial flag.");
undated.StartSceneID = null; undated.InitialBookID = 999;
Check(Snapshot(initialInput).Relationships.Count == 1, "Unplaced initial-book boundary must fail closed.");
undated.InitialBookID = null; undated.EndSceneID = 900;
Check(Snapshot(initialInput).Relationships.Count == 1, "Already-ended relationship must not be active.");
var later = Snapshot(scene: 1);
Check(later.Assets.Single(x => x.StoryAssetID == 10).Holder == "Outsider", "Later custody event must take effect only later.");
Check(later.Relationships[0].CurrentStateName == "Hostile", "Later relationship state must take effect.");
}
public static void ThreadsAndWarnings()
{
var point = Snapshot();
Check(point.Threads.Select(x => x.PlotThreadID).ToHashSet().SetEquals([1, 4]), "Active threads include earlier introduction and bounded event introduction; exclude future and already resolved threads.");
Check(point.Threads.Single(x => x.PlotThreadID == 1).State == "Introduced", "Global resolved status must not leak backwards.");
Check(point.Warnings.Any(x => x.CharacterID == 2 && x.WarningType.Contains("Unknown")), "Future location evidence must not suppress current unknown-location warning.");
Check(point.Warnings.All(x => x.SceneID != 1 && x.PreviousSceneId != 1), "Warnings must not contain future evidence.");
Check(Snapshot(scene: 1).Threads.All(x => x.PlotThreadID != 1), "Resolved-at-selected-scene thread is no longer active.");
var input = Fixture();
input.History.ThreadEvents.Single(x => x.PlotThreadID == 4).EventTypeName = "Resolved";
Check(Snapshot(input).Threads.All(x => x.PlotThreadID != 4), "Resolved event must close thread even when resolution field is absent.");
}
public static void OwnershipAndNoAi()
{
var input = Fixture(); int dataCalls = 0;
var access = Proxy<IProjectAccessService>((_, _) => Task.FromResult(false));
var timeline = Proxy<ITimelineRepository>((_, _) => { dataCalls++; return Task.FromResult(input.Timeline); });
var unusedCharacters = Proxy<ICharacterRepository>((_, _) => throw new Exception("Unexpected character query"));
var unusedAssets = Proxy<IAssetRepository>((_, _) => throw new Exception("Unexpected asset query"));
var unusedLocations = Proxy<ILocationRepository>((_, _) => throw new Exception("Unexpected location query"));
var unusedHistory = Proxy<IStoryPointRepository>((_, _) => throw new Exception("Unexpected history query"));
var unusedAcknowledgements = Proxy<IContinuityWarningAcknowledgementRepository>((_, _) => throw new Exception("Unexpected acknowledgement query"));
var service = new StoryPointContextService(access, timeline, unusedCharacters, unusedAssets, unusedLocations, unusedHistory, unusedAcknowledgements);
Check(service.GetAsync(1, 20).Result is null && dataCalls == 0, "Denied project must not load any data.");
access = Proxy<IProjectAccessService>((_, _) => Task.FromResult(true));
service = new StoryPointContextService(access, timeline, unusedCharacters, unusedAssets, unusedLocations, unusedHistory, unusedAcknowledgements);
Check(service.GetAsync(1, 9999).Result is null, "A scene outside the requested project must be rejected before loading history.");
var calls = new List<string>();
var characters = Proxy<ICharacterRepository>((method, _) => { calls.Add(method); return method switch {
nameof(ICharacterRepository.GetTimelineAsync) => Task.FromResult((input.Characters, input.Appearances)),
nameof(ICharacterRepository.GetRelationshipMapAsync) => Task.FromResult(input.Relationships),
_ => throw new Exception("Unexpected per-character query") }; });
var assets = Proxy<IAssetRepository>((method, _) => { calls.Add(method); return Task.FromResult((input.Assets, input.AssetEvents)); });
var locations = Proxy<ILocationRepository>((method, _) => { calls.Add(method); return Task.FromResult(input.Locations); });
var history = Proxy<IStoryPointRepository>((method, _) => { calls.Add(method); return Task.FromResult(input.History); });
var acknowledgements = Proxy<IContinuityWarningAcknowledgementRepository>((method, _) => { calls.Add(method); return Task.FromResult(input.Acknowledgements); });
service = new StoryPointContextService(access, timeline, characters, assets, locations, history, acknowledgements);
Check(service.GetAsync(1, 20).Result?.Snapshot?.Scene.SceneID == 20 && calls.Count == 6, "Page load must compose six history batches with no per-entity calls or AI dependency.");
Check(StoryPointContextService.Compose(input, 9999) is null, "Invalid scene projection must fail closed.");
var controller = new StoryPointController(service) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } };
controller.Request.Headers["X-Requested-With"] = "XMLHttpRequest";
Check(controller.Index(1, 20).Result is PartialViewResult { ViewName: "_Snapshot" }, "AJAX route must render reusable snapshot.");
Check(controller.Index(1, 9999).Result is NotFoundResult, "Controller must reject cross-project/unknown scene.");
Check(controller.Index(0, 20).Result is BadRequestObjectResult, "Missing project must fail.");
}
public static void CanonicalTieBreaks()
{
var input = Fixture();
input.Timeline.Books[0].SortOrder = 1; input.Timeline.Books[1].SortOrder = 1;
Check(StoryStateService.OrderScenes(input.Timeline).Select(x => x.SceneID).SequenceEqual([900, 20, 1]), "BookNumber tie breaker must match NarrativeSceneOrder, not input or ID order.");
Check(Snapshot(input).Relationships.Single().CurrentStateName == "Trusted", "Relationship projection must use the same canonical boundary.");
}
private static T Proxy<T>(Func<string, object?[]?, object?> invoke) where T : class
{ var proxy = DispatchProxy.Create<T, StoryPointProxy>(); ((StoryPointProxy)(object)proxy).Handler = invoke; return proxy; }
private static StoryPointInputs Fixture() => new()
{
Timeline = new TimelineData { Project = new Project { ProjectID = 1 },
Books = [new Book { BookID = 2, BookNumber = 2, SortOrder = 2, BookTitle = "Voyage" }, new Book { BookID = 9, BookNumber = 1, SortOrder = 1, BookTitle = "Departure" }],
Chapters = [new Chapter { ChapterID = 2, BookID = 2, ChapterNumber = 1, SortOrder = 1 }, new Chapter { ChapterID = 9, BookID = 9, ChapterNumber = 1, SortOrder = 1 }],
Scenes = [new Scene { SceneID = 1, ChapterID = 2, SortOrder = 2, SceneNumber = 2, PrimaryLocationID = 5 },
new Scene { SceneID = 20, ChapterID = 2, POVCharacterID = 1, SortOrder = 1, SceneNumber = 1, PrimaryLocationID = 5, TimeModeName = "Exact Date", StartDateTime = new DateTime(2001, 2, 5) },
new Scene { SceneID = 900, ChapterID = 9, SortOrder = 1, SceneNumber = 1, PrimaryLocationID = 5 }] },
Characters = [new Character { CharacterID = 1, CharacterName = "Ari", BirthDate = new DateTime(1981, 1, 5), AgeAtSeriesStart = 19 }, new Character { CharacterID = 2, CharacterName = "Bo" }],
Appearances = [new SceneCharacter { CharacterID = 1, CharacterName = "Ari", SceneID = 900, LocationID = 5, LocationPath = "Harbour" },
new SceneCharacter { CharacterID = 1, CharacterName = "Ari", SceneID = 20, LocationID = 5, LocationPath = "Harbour" },
new SceneCharacter { CharacterID = 2, CharacterName = "Bo", SceneID = 20 },
new SceneCharacter { CharacterID = 1, CharacterName = "Ari", SceneID = 1, LocationID = 6, LocationPath = "Tower" },
new SceneCharacter { CharacterID = 2, CharacterName = "Bo", SceneID = 1, LocationID = 6, LocationPath = "Tower" }],
Assets = [new StoryAsset { StoryAssetID = 10, AssetName = "Case" }, new StoryAsset { StoryAssetID = 11, AssetName = "Future object" }],
AssetEvents = [new AssetEvent { AssetEventID = 1, StoryAssetID = 10, SceneID = 900, ToStateName = "Sealed" }, new AssetEvent { AssetEventID = 2, StoryAssetID = 10, SceneID = 1, ToStateName = "Opened" }],
Locations = [new LocationItem { LocationID = 5, LocationName = "Harbour", LocationPath = "Harbour" }],
Relationships = new RelationshipMapData { Relationships = [
new CharacterRelationship { CharacterRelationshipID = 1, CharacterAID = 1, CharacterBID = 2, CharacterAName = "Ari", CharacterBName = "Bo" },
new CharacterRelationship { CharacterRelationshipID = 2, CharacterAID = 1, CharacterBID = 3 },
new CharacterRelationship { CharacterRelationshipID = 3, CharacterAID = 1, CharacterBID = 4 }],
RelationshipEvents = [new RelationshipEvent { CharacterRelationshipID = 1, SceneID = 900, RelationshipStateName = "Trusted" },
new RelationshipEvent { CharacterRelationshipID = 1, SceneID = 1, RelationshipStateName = "Hostile" },
new RelationshipEvent { CharacterRelationshipID = 2, SceneID = 1, RelationshipStateName = "Married" }] },
History = new StoryPointHistory {
Knowledge = [new CharacterKnowledgeItem { CharacterKnowledgeID = 1, CharacterID = 1, SceneID = 900, Description = "The gate code", KnowledgeStateName = "Knows" },
new CharacterKnowledgeItem { CharacterKnowledgeID = 2, CharacterID = 1, SceneID = 1, Description = "The gate code", KnowledgeStateName = "Unaware" },
new CharacterKnowledgeItem { CharacterKnowledgeID = 3, CharacterID = 2, SceneID = 1, Description = "Future secret", KnowledgeStateName = "Knows" }],
AssetLocations = [new SceneAssetLocation { StoryAssetID = 10, SceneID = 900, LocationID = 5, LocationPath = "Harbour" },
new SceneAssetLocation { StoryAssetID = 10, SceneID = 1, LocationID = 6, LocationPath = "Tower" }],
Custody = [new AssetCustodyEvent { AssetCustodyEventID = 1, StoryAssetID = 10, SceneID = 20, CustodianSummary = "Ari" },
new AssetCustodyEvent { AssetCustodyEventID = 2, StoryAssetID = 10, SceneID = 1, CustodianSummary = "Outsider" }],
Custodians = [new AssetCustodyCharacter { AssetCustodyEventID = 1, StoryAssetID = 10, SceneID = 20, CharacterID = 1 },
new AssetCustodyCharacter { AssetCustodyEventID = 2, StoryAssetID = 10, SceneID = 1, CharacterID = 3 }],
Threads = [new PlotThread { PlotThreadID = 1, ThreadTitle = "Open question", ThreadTypeName = "Question", IntroducedSceneID = 900, ActualResolutionSceneID = 1, ThreadStatusName = "Resolved" },
new PlotThread { PlotThreadID = 2, ThreadTitle = "Future", IntroducedSceneID = 1 },
new PlotThread { PlotThreadID = 3, ThreadTitle = "Closed", IntroducedSceneID = 900, ActualResolutionSceneID = 900 },
new PlotThread { PlotThreadID = 4, ThreadTitle = "Clue", ThreadTypeName = "Clue" }],
ThreadEvents = [new ThreadEvent { PlotThreadID = 4, SceneID = 20, EventTypeName = "Introduced" }] }
};
}
public class StoryPointProxy : DispatchProxy
{
public Func<string, object?[]?, object?> Handler { get; set; } = null!;
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) => Handler(targetMethod!.Name, args);
}

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<key id="49c1e5c2-21a2-4a05-8080-08fa2b0dd614" version="1">
<creationDate>2026-09-12T18:32:05.1596463Z</creationDate>
<activationDate>2026-09-12T18:32:05.1596463Z</activationDate>
<expirationDate>2026-12-11T18:32:05.1596463Z</expirationDate>
<descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=10.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<descriptor>
<encryption algorithm="AES_256_CBC" />
<validation algorithm="HMACSHA256" />
<masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
<!-- Warning: the key below is in an unencrypted form. -->
<value>7wzHKkUnWgH96NmDXrAbiYoKOD0O5wQ18zR38r7ouCpJrN2EP3p8cE5JPO7nqaScKovUoH/iRzophulwPFgm2A==</value>
</masterKey>
</descriptor>
</descriptor>
</key>

View File

@ -1,7 +1,5 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
using PlotLine.Data;
using PlotLine.Models; using PlotLine.Models;
using PlotLine.Services; using PlotLine.Services;
using PlotLine.ViewModels; using PlotLine.ViewModels;
@ -23,19 +21,8 @@ public sealed class AdminController(
IStoryIntelligenceExistingChapterQueueService existingChapterQueue, IStoryIntelligenceExistingChapterQueueService existingChapterQueue,
IStoryIntelligenceFullChapterTestService fullChapterTest, IStoryIntelligenceFullChapterTestService fullChapterTest,
IStoryIntelligenceImportCommitService storyIntelligenceImportCommit, IStoryIntelligenceImportCommitService storyIntelligenceImportCommit,
INameIntelligenceService nameIntelligence, IIllustrationLibraryService illustrationLibrary) : Controller
IIllustrationLibraryService illustrationLibrary,
IProjectRepository projects,
IBookRepository books,
IOnboardingBuildRepository onboardingBuilds,
ICoreImportBasicLocationService coreImportBasicLocations,
IWebHostEnvironment environment,
ILogger<AdminController> logger) : Controller
{ {
private static readonly Guid DiscoveryBasicLocationReviewPreviewId = Guid.Parse("50760000-3068-4000-8000-000000005076");
private const int DiscoveryProjectId = 3068;
private const int DiscoveryBookId = 5076;
[HttpGet("")] [HttpGet("")]
[HttpGet("index")] [HttpGet("index")]
public async Task<IActionResult> Index() public async Task<IActionResult> Index()
@ -52,138 +39,6 @@ public sealed class AdminController(
return View(); return View();
} }
[HttpGet("development/core-basic-location-review/discovery")]
public async Task<IActionResult> DevelopmentDiscoveryBasicLocationReview(bool reset = true)
{
if (!environment.IsDevelopment())
{
return NotFound();
}
if (currentUser.UserId is not int userId)
{
return Forbid();
}
var project = await projects.GetAsync(DiscoveryProjectId);
var book = await books.GetAsync(DiscoveryBookId);
if (project is null || book is null || book.ProjectID != DiscoveryProjectId)
{
return NotFound();
}
var sceneSources = await onboardingBuilds.ListSceneManuscriptSourcesByBookAsync(DiscoveryBookId);
if (sceneSources.Count == 0)
{
TempData["AdminError"] = "Book 5076 has no canonical scene source text to prepare.";
return RedirectToAction(nameof(Index));
}
if (reset)
{
await coreImportBasicLocations.ResetAndPrepareAsync(userId, DiscoveryBasicLocationReviewPreviewId, DiscoveryProjectId, DiscoveryBookId, null, sceneSources);
}
else
{
await coreImportBasicLocations.EnsurePreparedAsync(userId, DiscoveryBasicLocationReviewPreviewId, DiscoveryProjectId, DiscoveryBookId, null, sceneSources);
}
var model = await coreImportBasicLocations.GetReviewAsync(
userId,
DiscoveryBasicLocationReviewPreviewId,
"Discovery retained Book 5076",
project.ProjectName,
book.BookDisplayTitle);
if (model is null)
{
return NotFound();
}
return View("~/Views/Onboarding/ScanLocations.cshtml", new CoreImportBasicLocationReviewViewModel
{
PreviewID = model.PreviewID,
ReviewID = model.ReviewID,
Status = model.Status,
DocumentTitle = model.DocumentTitle,
SelectedProjectName = model.SelectedProjectName,
SelectedBookTitle = model.SelectedBookTitle,
FormController = "Admin",
FormAction = nameof(SaveDevelopmentDiscoveryBasicLocationReview),
ContinueButtonText = "Save selected locations",
RawObservationCount = model.RawObservationCount,
CandidateCount = model.CandidateCount,
Candidates = model.Candidates,
ExistingLocationOptions = model.ExistingLocationOptions
});
}
[HttpPost("development/core-basic-location-review/discovery")]
[RequestFormLimits(ValueCountLimit = 20000, KeyLengthLimit = 4096, ValueLengthLimit = 1024 * 1024)]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SaveDevelopmentDiscoveryBasicLocationReview(CoreImportBasicLocationReviewForm form, string intent = "save")
{
if (!environment.IsDevelopment())
{
return NotFound();
}
if (currentUser.UserId is not int userId)
{
return Forbid();
}
if (form.PreviewID != DiscoveryBasicLocationReviewPreviewId)
{
return BadRequest();
}
if (string.Equals(intent, "skip", StringComparison.OrdinalIgnoreCase))
{
await coreImportBasicLocations.SkipAsync(userId, form.PreviewID);
TempData["AdminMessage"] = "Development basic location review skipped for Book 5076.";
return RedirectToAction(nameof(Index));
}
await coreImportBasicLocations.SaveDecisionsAsync(userId, form);
if (string.Equals(intent, "continue", StringComparison.OrdinalIgnoreCase))
{
await coreImportBasicLocations.CompleteAsync(userId, form.PreviewID);
TempData["AdminMessage"] = "Development basic location review materialised selected locations for Book 5076.";
return RedirectToAction(nameof(Index));
}
TempData["OnboardingReviewMessage"] = "Development location review choices saved.";
return RedirectToAction(nameof(DevelopmentDiscoveryBasicLocationReview), new { reset = false });
}
[HttpPost("name-intelligence/validate")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ValidateNameIntelligence(CancellationToken cancellationToken)
{
var result = await nameIntelligence.ValidatePackageAsync(cancellationToken);
TempData[result.Succeeded ? "AdminMessage" : "AdminError"] = $"{result.Message} Locales={result.LocaleRows:N0}, names={result.GivenNameRows:N0}, relationships={result.RelationshipRows:N0}, usages={result.UsageRows:N0}.";
if (result.Errors.Count > 0)
{
TempData["AdminError"] = string.Join(" ", result.Errors.Take(5));
}
return RedirectToAction(nameof(Index));
}
[HttpPost("name-intelligence/import")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ImportNameIntelligence(bool force, CancellationToken cancellationToken)
{
var result = await nameIntelligence.EnsureReferenceDataImportedAsync(force, cancellationToken);
TempData[result.Succeeded ? "AdminMessage" : "AdminError"] = $"{result.Message} Package {result.PackageId} {result.Version}: locales={result.LocaleRows:N0}, names={result.GivenNameRows:N0}, relationships={result.RelationshipRows:N0}, usages={result.UsageRows:N0}.";
if (result.Errors.Count > 0)
{
TempData["AdminError"] = string.Join(" ", result.Errors.Take(5));
}
return RedirectToAction(nameof(Index));
}
[HttpGet("story-intelligence/illustration-library")] [HttpGet("story-intelligence/illustration-library")]
public async Task<IActionResult> StoryIntelligenceIllustrationLibrary([FromQuery] IllustrationLibraryFilter filter) public async Task<IActionResult> StoryIntelligenceIllustrationLibrary([FromQuery] IllustrationLibraryFilter filter)
{ {
@ -314,27 +169,8 @@ public sealed class AdminController(
return Forbid(); return Forbid();
} }
logger.LogWarning("Story Intelligence development reset requested by admin user {UserId}.", userId); var result = await storyIntelligenceDevelopmentReset.ResetOwnedProjectsAndImportsAsync(userId, cancellationToken);
TempData[result.Success ? "AdminMessage" : "AdminError"] = result.Message;
try
{
var result = await storyIntelligenceDevelopmentReset.ResetOwnedProjectsAndImportsAsync(userId, cancellationToken);
TempData[result.Success ? "AdminMessage" : "AdminError"] = result.Message;
logger.LogWarning(
"Story Intelligence development reset finished for admin user {UserId}. Success={Success} DeletedProjects={DeletedProjects} RemainingProjects={RemainingProjects} RemainingImports={RemainingImports} RemainingRuns={RemainingRuns}.",
userId,
result.Success,
result.DeletedProjects,
result.RemainingProjects,
result.RemainingImportSessions,
result.RemainingRuns);
}
catch (Exception ex)
{
logger.LogError(ex, "Story Intelligence development reset failed for admin user {UserId}.", userId);
TempData["AdminError"] = "Story Intelligence reset failed. No files were removed; check the application logs for the blocking dependency.";
}
return RedirectToAction(nameof(StoryIntelligenceDiagnostics)); return RedirectToAction(nameof(StoryIntelligenceDiagnostics));
} }

View File

@ -69,8 +69,6 @@ public sealed class BooksController(IBookService books, IOnboardingService onboa
createModel.BookNumber = model.BookNumber; createModel.BookNumber = model.BookNumber;
createModel.Description = model.Description; createModel.Description = model.Description;
createModel.TargetWordCount = model.TargetWordCount; createModel.TargetWordCount = model.TargetWordCount;
createModel.StoryEra = model.StoryEra;
createModel.PrimaryLocaleCode = model.PrimaryLocaleCode;
createModel.WritingStartDate = model.WritingStartDate; createModel.WritingStartDate = model.WritingStartDate;
createModel.TargetCompletionDate = model.TargetCompletionDate; createModel.TargetCompletionDate = model.TargetCompletionDate;
createModel.PublicationDate = model.PublicationDate; createModel.PublicationDate = model.PublicationDate;

View File

@ -51,7 +51,6 @@ public sealed class ChaptersController(IChapterService chapters) : Controller
createModel.ChapterNumber = model.ChapterNumber; createModel.ChapterNumber = model.ChapterNumber;
createModel.ChapterTitle = model.ChapterTitle; createModel.ChapterTitle = model.ChapterTitle;
createModel.POVCharacterID = model.POVCharacterID;
createModel.Summary = model.Summary; createModel.Summary = model.Summary;
createModel.RevisionStatusID = model.RevisionStatusID; createModel.RevisionStatusID = model.RevisionStatusID;
return View("Edit", createModel); return View("Edit", createModel);
@ -60,15 +59,6 @@ public sealed class ChaptersController(IChapterService chapters) : Controller
return RedirectToAction(nameof(Details), new { id = chapterId }); return RedirectToAction(nameof(Details), new { id = chapterId });
} }
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SetScenesPov(int chapterId, int? povCharacterId, bool overwriteAuthor)
{
var updated = await chapters.SetScenesPovAsync(chapterId, povCharacterId, overwriteAuthor);
TempData["ChapterMessage"] = $"POV updated on {updated:N0} scene{(updated == 1 ? string.Empty : "s")}.";
return RedirectToAction(nameof(Details), new { id = chapterId });
}
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> Archive(int id, int bookId) public async Task<IActionResult> Archive(int id, int bookId)

View File

@ -32,12 +32,6 @@ public sealed class CharactersController(ICharacterService characters) : Control
return model is null ? NotFound() : View(model); return model is null ? NotFound() : View(model);
} }
public async Task<IActionResult> Merge(int id)
{
var model = await characters.GetMergeAsync(id);
return model is null ? NotFound() : View(model);
}
public async Task<IActionResult> CropAvatar(int imageId) public async Task<IActionResult> CropAvatar(int imageId)
{ {
var model = await characters.GetAvatarCropAsync(imageId); var model = await characters.GetAvatarCropAsync(imageId);
@ -105,93 +99,6 @@ public sealed class CharactersController(ICharacterService characters) : Control
: RedirectToAction(nameof(Details), new { id = saveResult.CharacterID }); : RedirectToAction(nameof(Details), new { id = saveResult.CharacterID });
} }
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Merge(CharacterMergeForm form)
{
try
{
var result = await characters.MergeAsync(form);
if (result is null)
{
return NotFound();
}
TempData["CharacterMessage"] = $"{result.SourceCharacterName} merged into {result.TargetCharacterName}.";
return RedirectToAction(nameof(Details), new { id = result.TargetCharacterID });
}
catch (InvalidOperationException ex)
{
ModelState.AddModelError(string.Empty, ex.Message);
var model = await characters.GetMergeAsync(form.SourceCharacterID);
if (model is null)
{
return NotFound();
}
model.Form = form;
return View(model);
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SetPreferredName(int id, string preferredName)
{
try
{
await characters.SetPreferredNameAsync(id, preferredName);
TempData["CharacterMessage"] = $"Preferred name set to {preferredName}.";
}
catch (InvalidOperationException ex)
{
TempData["CharacterMessage"] = ex.Message;
}
return RedirectToAction(nameof(Details), new { id });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Reanalyse(int id)
{
try
{
var run = await characters.QueueCharacterReanalysisAsync(id);
if (run is null)
{
return NotFound();
}
TempData["CharacterMessage"] = run.Status == "Running"
? "Character analysis is already running."
: "Character reanalysis queued.";
if (Request.Headers.XRequestedWith == "XMLHttpRequest")
{
return Json(new
{
success = true,
characterEnrichmentRunID = run.CharacterEnrichmentRunID,
bookID = run.BookID,
characterID = run.CharacterID,
status = run.Status,
currentMessage = run.CurrentMessage
});
}
}
catch (InvalidOperationException ex)
{
TempData["CharacterMessage"] = ex.Message;
if (Request.Headers.XRequestedWith == "XMLHttpRequest")
{
return BadRequest(new { success = false, message = ex.Message });
}
}
return RedirectToAction(nameof(Details), new { id });
}
private void ValidateSexWorkflow(CharacterEditViewModel model) private void ValidateSexWorkflow(CharacterEditViewModel model)
{ {
if (model.SexValueID == CharacterEditViewModel.CustomSexValue) if (model.SexValueID == CharacterEditViewModel.CustomSexValue)

View File

@ -0,0 +1,193 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PlotLine.Data;
using PlotLine.Models;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
[Authorize(Policy = "AdminOnly")]
[Route("Development")]
public sealed class DevelopmentController(
IWebHostEnvironment environment,
ICurrentUserService currentUser,
IIllustrationLibraryService illustrationLibrary,
IStoryMemoryService storyMemory,
IStoryMemoryRepository storyMemoryRepository,
IStoryIntelligenceVisualisationSnapshotService snapshots) : Controller
{
[HttpGet("StoryIntelligenceExperience")]
public async Task<IActionResult> StoryIntelligenceExperience([FromQuery] string? mode, [FromQuery] int? importSessionId, [FromQuery] int? runId)
{
if (!environment.IsDevelopment())
{
return NotFound();
}
var requestedMode = string.IsNullOrWhiteSpace(mode) ? "real" : mode.Trim();
var simulationRequested = string.Equals(requestedMode, "simulation", StringComparison.OrdinalIgnoreCase);
var replayRequested = string.Equals(requestedMode, "replay", StringComparison.OrdinalIgnoreCase);
if (!simulationRequested && importSessionId.HasValue && currentUser.UserId is int userId)
{
var realModel = replayRequested
? await snapshots.BuildReplaySnapshotAsync(importSessionId.Value, userId)
: await snapshots.BuildImportSessionSnapshotAsync(importSessionId.Value, userId);
if (realModel is null)
{
return View(ErrorModel(
requestedMode,
importSessionId,
$"Import Session {importSessionId.Value:N0} could not be found for the current user, or it has no persisted chapter analysis runs yet."));
}
return View(realModel);
}
if (!simulationRequested && runId.HasValue && currentUser.UserId is int legacyUserId)
{
var realModel = await snapshots.BuildSnapshotAsync(runId.Value, legacyUserId);
if (realModel is null)
{
return View(ErrorModel(
requestedMode,
null,
$"Run {runId.Value:N0} could not be found for the current user. Live visualisation URLs should use importSessionId for book imports."));
}
return View(realModel);
}
if (simulationRequested)
{
var model = StoryIntelligenceExperiencePrototypeData.Build();
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
return View(model);
}
if (currentUser.UserId is int selectorUserId)
{
var sessions = await snapshots.ListImportSessionOptionsAsync(selectorUserId);
return View(new StoryIntelligenceExperiencePrototypeViewModel
{
Mode = "selector",
AvailableRuns = sessions,
ProjectName = "Development",
BookTitle = "Story Intelligence",
SnapshotMessage = sessions.Count == 0
? "No persisted Story Intelligence import sessions were found for the current user. Start a real import, then refresh this page."
: "Choose a Story Intelligence import session, or explicitly open simulation mode.",
Diagnostics = new StoryIntelligenceExperienceSnapshotDiagnostics
{
RequestedMode = requestedMode,
RequestedImportSessionId = importSessionId,
SnapshotSource = "Real"
}
});
}
return View(ErrorModel(requestedMode, importSessionId, "No development user is signed in."));
}
[HttpPost("StoryIntelligenceExperience/Rebuild")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RebuildStoryIntelligenceExperience(
[FromForm] int importSessionId,
[FromForm] string? rebuildScope,
[FromForm] bool dryRun = false,
[FromForm] bool queueDemand = false,
[FromForm] int? maxDemandToQueue = null,
[FromForm] string? appearancePreference = null)
{
if (!environment.IsDevelopment())
{
return NotFound();
}
if (importSessionId <= 0 || currentUser.UserId is not int userId)
{
return BadRequest("A valid importSessionId is required.");
}
var rebuild = await storyMemory.RebuildImportSessionAsync(
importSessionId,
userId,
new StoryMemoryRebuildOptions
{
DryRun = dryRun,
RebuildAll = !string.Equals(rebuildScope, "incremental", StringComparison.OrdinalIgnoreCase),
QueueDemand = queueDemand,
MaxDemandToQueue = maxDemandToQueue,
AppearancePreference = string.IsNullOrWhiteSpace(appearancePreference)
? StoryMemoryAppearancePreferences.Balanced
: appearancePreference.Trim()
},
HttpContext.RequestAborted);
if (rebuild.DryRun)
{
TempData["StoryIntelligenceReplayMessage"] = $"Dry run complete. {rebuild.SceneResultsRead:N0} stored scene result(s), {rebuild.SceneResultsProcessed:N0} parseable. No database changes, AI calls, or image credits.";
}
else
{
TempData["StoryIntelligenceReplayMessage"] = $"Durable Story Memory rebuilt. {rebuild.SceneResultsProcessed:N0} scene result(s) processed, {rebuild.AssignmentsCreatedOrValidated:N0} assignment(s), {rebuild.DemandsCreated:N0} demand record(s), {rebuild.AiCalls:N0} AI calls.";
}
return RedirectToAction(nameof(StoryIntelligenceExperience), new { importSessionId, mode = "replay" });
}
[HttpGet("StoryMemoryDiagnostics")]
public async Task<IActionResult> StoryMemoryDiagnostics([FromQuery] int importSessionId)
{
if (!environment.IsDevelopment())
{
return NotFound();
}
if (importSessionId <= 0)
{
return BadRequest("A valid importSessionId is required.");
}
var diagnostics = await storyMemoryRepository.GetDiagnosticsAsync(importSessionId);
return Json(diagnostics);
}
[HttpGet("StoryIntelligenceIllustrationDiagnostics")]
public async Task<IActionResult> StoryIntelligenceIllustrationDiagnostics([FromQuery] int importSessionId)
{
if (!environment.IsDevelopment())
{
return NotFound();
}
if (importSessionId <= 0 || currentUser.UserId is not int userId)
{
return BadRequest("A valid importSessionId is required.");
}
var model = await snapshots.BuildImportSessionSnapshotAsync(importSessionId, userId);
if (model is null)
{
return NotFound($"Import Session {importSessionId:N0} could not be found for the current user, or it has no persisted chapter analysis runs yet.");
}
return View(model);
}
private static StoryIntelligenceExperiencePrototypeViewModel ErrorModel(string requestedMode, int? importSessionId, string message)
=> new()
{
Mode = "error",
ProjectName = "Development",
BookTitle = "Story Intelligence",
SnapshotMessage = message,
Diagnostics = new StoryIntelligenceExperienceSnapshotDiagnostics
{
RequestedMode = requestedMode,
RequestedImportSessionId = importSessionId,
SnapshotSource = "Real",
Warning = message
}
};
}

View File

@ -32,64 +32,6 @@ public sealed class LocationsController(ILocationService locations) : Controller
return model is null ? NotFound() : View(model); return model is null ? NotFound() : View(model);
} }
public async Task<IActionResult> Merge(int id)
{
var model = await locations.GetMergeAsync(id);
return model is null ? NotFound() : View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Merge([Bind(Prefix = "Form")] EntityMergeForm form)
{
if (ModelState.IsValid)
{
try
{
var result = await locations.MergeAsync(form);
if (result is null) return NotFound();
TempData["LocationMessage"] = $"{result.SourceName} merged into {result.TargetName}.";
return RedirectToAction(nameof(Details), new { id = result.TargetID });
}
catch (InvalidOperationException ex)
{
ModelState.AddModelError(string.Empty, ex.Message);
}
}
var model = await locations.GetMergeAsync(form.SourceID);
if (model is null) return NotFound();
model.Form = form;
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Reparent(int projectId, int locationId, int parentLocationId, int? expectedParentLocationId)
{
if (!ModelState.IsValid)
{
if (IsAjaxRequest()) return BadRequest(new { error = "Invalid location selection. Please try again." });
TempData["LocationError"] = "Invalid location selection. Please try again.";
return RedirectToAction(nameof(Index), new { projectId });
}
try
{
await locations.ReparentAsync(projectId, locationId, parentLocationId, expectedParentLocationId);
if (IsAjaxRequest())
{
var model = await locations.GetLocationsAsync(projectId);
return model is null ? NotFound() : PartialView("_LocationTree", model);
}
TempData["LocationMessage"] = "Location moved to its new parent.";
}
catch (InvalidOperationException ex)
{
if (IsAjaxRequest()) return BadRequest(new { error = ex.Message });
TempData["LocationError"] = ex.Message;
}
return RedirectToAction(nameof(Index), new { projectId });
}
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> Save(LocationEditViewModel model) public async Task<IActionResult> Save(LocationEditViewModel model)
@ -119,23 +61,6 @@ public sealed class LocationsController(ILocationService locations) : Controller
return RedirectToAction(nameof(Details), new { id = locationId }); return RedirectToAction(nameof(Details), new { id = locationId });
} }
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SetPreferredName(int id, string preferredName)
{
try
{
await locations.SetPreferredNameAsync(id, preferredName);
TempData["LocationMessage"] = $"Preferred name set to {preferredName}.";
}
catch (InvalidOperationException ex)
{
TempData["LocationMessage"] = ex.Message;
}
return RedirectToAction(nameof(Details), new { id });
}
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> Archive(int id, int projectId) public async Task<IActionResult> Archive(int id, int projectId)
@ -157,26 +82,14 @@ public sealed class LocationsController(ILocationService locations) : Controller
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> DropRelationship(LocationRelationshipEditViewModel model) public async Task<IActionResult> DropRelationship(LocationRelationshipEditViewModel model)
{ {
if (!ModelState.IsValid || model.FromLocationID == model.ToLocationID) if (model.FromLocationID != model.ToLocationID)
{
if (IsAjaxRequest()) return BadRequest(new { error = "Choose two different locations and a relationship type." });
return RedirectToAction(nameof(Index), new { projectId = model.ProjectID });
}
try
{ {
await locations.SaveRelationshipAsync(model); await locations.SaveRelationshipAsync(model);
if (IsAjaxRequest()) return Json(new { message = "Location relationship saved." });
}
catch (InvalidOperationException ex)
{
if (IsAjaxRequest()) return BadRequest(new { error = ex.Message });
TempData["LocationError"] = ex.Message;
} }
return RedirectToAction(nameof(Index), new { projectId = model.ProjectID }); return RedirectToAction(nameof(Index), new { projectId = model.ProjectID });
} }
private bool IsAjaxRequest() => Request.Headers["X-Requested-With"] == "XMLHttpRequest";
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> ArchiveRelationship(int id, int returnLocationId) public async Task<IActionResult> ArchiveRelationship(int id, int returnLocationId)

View File

@ -1,21 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PlotLine.Models;
using PlotLine.Services;
namespace PlotLine.Controllers;
[Authorize]
public sealed class NamesController(INameIntelligenceService names) : Controller
{
public async Task<IActionResult> Index([FromQuery] NameSearchFilter filter, CancellationToken cancellationToken)
{
if (filter.ProjectID <= 0)
{
return BadRequest("ProjectID is required.");
}
var model = await names.BuildLibraryAsync(filter, cancellationToken);
return model is null ? NotFound() : View(model);
}
}

View File

@ -3,19 +3,12 @@ using Microsoft.AspNetCore.Mvc;
using PlotLine.Models; using PlotLine.Models;
using PlotLine.Services; using PlotLine.Services;
using PlotLine.ViewModels; using PlotLine.ViewModels;
using System.Diagnostics;
namespace PlotLine.Controllers; namespace PlotLine.Controllers;
[Authorize] [Authorize]
[Route("onboarding")] [Route("onboarding")]
public sealed class OnboardingController( public sealed class OnboardingController(IOnboardingService onboarding, IOnboardingStoryIntelligenceService storyIntelligence) : Controller
IOnboardingService onboarding,
IOnboardingStoryIntelligenceService storyIntelligence,
IWholeBookPlotIntelligenceService wholeBookPlotIntelligence,
ICharacterEnrichmentService characterEnrichment,
ICurrentUserService currentUser,
ILogger<OnboardingController> logger) : Controller
{ {
[HttpGet("")] [HttpGet("")]
public async Task<IActionResult> Index() public async Task<IActionResult> Index()
@ -67,85 +60,11 @@ public sealed class OnboardingController(
return RedirectToAction(nameof(Index)); return RedirectToAction(nameof(Index));
} }
var build = await onboarding.GetBuildResultAsync(model.PreviewID);
if (build is null || build.SceneMappings.Count == 0)
{
TempData["ArchiveError"] = "Import the approved chapters and scenes before reviewing characters.";
await onboarding.SetCurrentStepAsync(OnboardingSteps.NextPathPreview);
return RedirectToAction(nameof(Index));
}
if (!await onboarding.IsBasicLocationReviewCompleteAsync(model.PreviewID))
{
TempData["OnboardingReviewMessage"] = "Review or skip basic locations before reviewing characters.";
return RedirectToAction(nameof(ScanLocations), new { previewId = model.PreviewID });
}
await onboarding.EnsureCharacterCandidatesAfterLocationReviewAsync(model.PreviewID);
model = await onboarding.GetScanReviewAsync(model.PreviewID);
if (model is null)
{
return NotFound();
}
ViewData["ScanReviewStep"] = "characters"; ViewData["ScanReviewStep"] = "characters";
ViewData["CharacterReviewPage"] = Math.Max(1, page); ViewData["CharacterReviewPage"] = Math.Max(1, page);
return View("ScanReview", model); return View("ScanReview", model);
} }
[HttpGet("scan-locations")]
[HttpGet("scan/locations")]
public async Task<IActionResult> ScanLocations(Guid previewId)
{
var model = await onboarding.GetBasicLocationReviewAsync(previewId);
if (model is null)
{
TempData["ArchiveError"] = "Import the approved chapters and scenes before reviewing locations.";
await onboarding.SetCurrentStepAsync(OnboardingSteps.NextPathPreview);
return RedirectToAction(nameof(Index));
}
if (model.IsComplete)
{
return RedirectToAction(nameof(ScanCharacters), new { previewId });
}
return View(model);
}
[HttpPost("scan-locations")]
[HttpPost("scan/locations")]
[RequestFormLimits(ValueCountLimit = 20000, KeyLengthLimit = 4096, ValueLengthLimit = 1024 * 1024)]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SaveBasicLocations(CoreImportBasicLocationReviewForm form, string intent = "save")
{
try
{
if (string.Equals(intent, "skip", StringComparison.OrdinalIgnoreCase))
{
await onboarding.SkipBasicLocationReviewAsync(form.PreviewID);
TempData["OnboardingReviewMessage"] = "Basic location review skipped. Next: review detected characters.";
return RedirectToAction(nameof(ScanCharacters), new { previewId = form.PreviewID });
}
if (string.Equals(intent, "continue", StringComparison.OrdinalIgnoreCase))
{
await onboarding.CompleteBasicLocationReviewAsync(form);
TempData["OnboardingReviewMessage"] = "Locations saved. Next: review detected characters.";
return RedirectToAction(nameof(ScanCharacters), new { previewId = form.PreviewID });
}
await onboarding.SaveBasicLocationReviewAsync(form);
TempData["OnboardingReviewMessage"] = "Location review choices saved.";
return RedirectToAction(nameof(ScanLocations), new { previewId = form.PreviewID });
}
catch (InvalidOperationException ex)
{
TempData["ArchiveError"] = ex.Message;
return RedirectToAction(nameof(ScanLocations), new { previewId = form.PreviewID });
}
}
[HttpGet("build-complete")] [HttpGet("build-complete")]
public async Task<IActionResult> BuildComplete(Guid previewId) public async Task<IActionResult> BuildComplete(Guid previewId)
{ {
@ -157,31 +76,18 @@ public sealed class OnboardingController(
public async Task<IActionResult> StoryIntelligence() public async Task<IActionResult> StoryIntelligence()
{ {
var model = await storyIntelligence.GetOverviewAsync(); var model = await storyIntelligence.GetOverviewAsync();
if (model.ExistingBatchID.HasValue) return View(model);
{
return RedirectToBook(model);
}
if (model.PreviewID == Guid.Empty)
{
return RedirectToAction(nameof(Index));
}
return RedirectToAction(nameof(BuildComplete), new { previewId = model.PreviewID });
} }
[HttpGet("story-intelligence/context")] [HttpGet("story-intelligence/context")]
public async Task<IActionResult> StoryIntelligenceContext() public async Task<IActionResult> StoryIntelligenceContext()
{ {
return await StoryIntelligence(); var model = await storyIntelligence.GetOverviewAsync();
return View(model);
} }
[HttpGet("story-intelligence/book/{bookId:int}/continue")] [HttpGet("story-intelligence/book/{bookId:int}/continue")]
public async Task<IActionResult> ContinueStoryIntelligence(int bookId) public async Task<IActionResult> ContinueStoryIntelligence(int bookId)
=> await StoryIntelligenceBookReviewCentre(bookId);
[HttpGet("story-intelligence/book/{bookId:int}/review-centre")]
public async Task<IActionResult> StoryIntelligenceBookReviewCentre(int bookId)
{ {
var target = await storyIntelligence.ResumeBookAsync(bookId); var target = await storyIntelligence.ResumeBookAsync(bookId);
if (target is null) if (target is null)
@ -192,117 +98,32 @@ public sealed class OnboardingController(
return target.Route switch return target.Route switch
{ {
StoryIntelligenceResumeRoutes.ReviewCentre => await StoryIntelligenceReviewCentre(target.BatchID), StoryIntelligenceResumeRoutes.Complete => RedirectToAction(nameof(StoryIntelligenceComplete), new { batchId = target.BatchID }),
StoryIntelligenceResumeRoutes.Complete => await StoryIntelligenceReviewCentre(target.BatchID), StoryIntelligenceResumeRoutes.Knowledge => RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = target.BatchID }),
StoryIntelligenceResumeRoutes.Narrative => await StoryIntelligenceReviewCentre(target.BatchID), StoryIntelligenceResumeRoutes.Relationships => RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId = target.BatchID }),
StoryIntelligenceResumeRoutes.Knowledge => await StoryIntelligenceReviewCentre(target.BatchID), StoryIntelligenceResumeRoutes.Assets => RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId = target.BatchID }),
StoryIntelligenceResumeRoutes.Relationships => await StoryIntelligenceReviewCentre(target.BatchID), StoryIntelligenceResumeRoutes.Locations => RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = target.BatchID }),
StoryIntelligenceResumeRoutes.Assets => await StoryIntelligenceReviewCentre(target.BatchID), StoryIntelligenceResumeRoutes.Characters => RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId = target.BatchID }),
StoryIntelligenceResumeRoutes.Locations => await StoryIntelligenceReviewCentre(target.BatchID), _ => RedirectToAction(nameof(StoryIntelligenceReview), new { batchId = target.BatchID })
StoryIntelligenceResumeRoutes.Characters => await StoryIntelligenceReviewCentre(target.BatchID),
_ => RedirectToAction("Details", "Books", new { id = bookId })
}; };
} }
[HttpPost("story-intelligence/start")] [HttpPost("story-intelligence/start")]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> StartStoryIntelligence() public async Task<IActionResult> StartStoryIntelligence(
=> RedirectToAction(nameof(StoryIntelligencePlotContext)); [FromForm] string? appearancePreference,
[FromForm] string? primaryAgeGroup,
[HttpGet("story-intelligence/plot-context")] [FromForm] string? storyEra,
public async Task<IActionResult> StoryIntelligencePlotContext() [FromForm] string? storyLocation)
{ {
var overview = await storyIntelligence.GetOverviewAsync(); var job = await storyIntelligence.StartAsync(appearancePreference, primaryAgeGroup, storyEra, storyLocation);
if (!overview.BookID.HasValue || !overview.CanStart)
{
TempData["OnboardingStoryIntelligenceError"] = "Review the manuscript scan before starting Story Intelligence.";
return RedirectToAction(nameof(Index));
}
var model = await wholeBookPlotIntelligence.GetAuthorPlotContextAsync(overview.BookID.Value);
if (model is null)
{
return RedirectToAction(nameof(Index));
}
return View("StoryIntelligenceAuthorPlotContext", model);
}
[HttpGet("story-intelligence/book/{bookId:int}/plot-context")]
public async Task<IActionResult> StoryIntelligenceBookPlotContext(int bookId)
{
var existing = await storyIntelligence.ResumeBookAsync(bookId);
if (existing is not null)
{
TempData["ArchiveMessage"] = "Story Intelligence is already available for this book. Progress is shown in the page header.";
return RedirectToAction("Details", "Books", new { id = bookId });
}
var model = await wholeBookPlotIntelligence.GetAuthorPlotContextAsync(bookId);
if (model is null)
{
TempData["ArchiveError"] = "Story Intelligence could not find that book.";
return RedirectToAction("Details", "Books", new { id = bookId });
}
return View("StoryIntelligenceAuthorPlotContext", model);
}
[HttpPost("story-intelligence/plot-context/start")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> StartStoryIntelligenceWithPlotContext(StoryIntelligenceAuthorPlotContextForm form)
{
if (form.ProjectID <= 0 || form.BookID <= 0)
{
TempData["OnboardingStoryIntelligenceError"] = "Story Intelligence could not find the selected book.";
return RedirectToAction(nameof(Index));
}
await wholeBookPlotIntelligence.SaveAuthorPlotContextAsync(form.ProjectID, form.BookID, form.PlotSeeds);
OnboardingStoryIntelligenceBatch? job;
try
{
var overview = await storyIntelligence.GetOverviewAsync();
var isCurrentOnboardingBook = overview.BookID == form.BookID && overview.PreviewID != Guid.Empty && overview.CanStart;
job = isCurrentOnboardingBook
? await storyIntelligence.StartAsync()
: await storyIntelligence.StartBookAsync(form.BookID);
}
catch (InvalidOperationException ex)
{
TempData["ArchiveError"] = ex.Message;
return RedirectToAction("Details", "Books", new { id = form.BookID });
}
if (job is null) if (job is null)
{ {
TempData["OnboardingStoryIntelligenceError"] = "Review the manuscript scan before starting Story Intelligence."; TempData["OnboardingStoryIntelligenceError"] = "Review the manuscript scan before starting Story Intelligence.";
return RedirectToAction("Details", "Books", new { id = form.BookID }); return RedirectToAction(nameof(StoryIntelligenceContext));
} }
var progress = await storyIntelligence.GetProgressAsync(job.BatchID); return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId = job.BatchID });
if (progress is not null)
{
TempData["ArchiveMessage"] = "Story Intelligence is now analysing your book. Progress is shown in the page header.";
return RedirectToBook(progress);
}
return RedirectToAction("Details", "Books", new { id = job.BookID });
}
[HttpPost("story-intelligence/book/{bookId:int}/start")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> StartBookStoryIntelligence(int bookId)
{
var existing = await storyIntelligence.ResumeBookAsync(bookId);
if (existing is not null)
{
TempData["ArchiveMessage"] = "Story Intelligence is already analysing this book. Progress is shown in the page header.";
return RedirectToAction("Details", "Books", new { id = bookId });
}
return RedirectToAction(nameof(StoryIntelligenceBookPlotContext), new { bookId });
} }
[HttpPost("story-intelligence/skip")] [HttpPost("story-intelligence/skip")]
@ -326,14 +147,7 @@ public sealed class OnboardingController(
public async Task<IActionResult> StoryIntelligenceProgress(Guid batchId) public async Task<IActionResult> StoryIntelligenceProgress(Guid batchId)
{ {
var model = await storyIntelligence.GetProgressAsync(batchId); var model = await storyIntelligence.GetProgressAsync(batchId);
if (model is null) return model is null ? NotFound() : View(model);
{
return NotFound();
}
return model.SceneAnalysisComplete
? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId })
: RedirectToBook(model);
} }
[HttpGet("story-intelligence/review")] [HttpGet("story-intelligence/review")]
@ -347,50 +161,18 @@ public sealed class OnboardingController(
if (model.HasActiveRuns) if (model.HasActiveRuns)
{ {
return RedirectToBook(model); return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
} }
return model.SceneAnalysisComplete return model.AllCommitted
? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId }) ? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId })
: RedirectToBook(model); : View(model);
}
[HttpGet("story-intelligence/review-centre")]
public async Task<IActionResult> StoryIntelligenceReviewCentre(Guid batchId)
{
var stopwatch = Stopwatch.StartNew();
var model = await storyIntelligence.GetReviewCentreAsync(batchId);
if (model is null)
{
return NotFound();
}
if (model.HasActiveRuns)
{
return RedirectToBook(model);
}
if (!model.SceneAnalysisComplete)
{
return RedirectToBook(model);
}
logger.LogInformation(
"Story Intelligence Review Centre GET rendered. BatchID={BatchID} ProjectID={ProjectID} BookID={BookID} ElapsedMs={ElapsedMs} ActiveRuns={ActiveRuns} SceneAnalysisComplete={SceneAnalysisComplete}",
batchId,
model.ProjectID,
model.BookID,
stopwatch.ElapsedMilliseconds,
model.HasActiveRuns,
model.SceneAnalysisComplete);
return View(nameof(StoryIntelligenceReviewCentre), model);
} }
[HttpGet("story-intelligence/characters")] [HttpGet("story-intelligence/characters")]
public async Task<IActionResult> StoryIntelligenceCharacters(Guid batchId, int page = 1) public async Task<IActionResult> StoryIntelligenceCharacters(Guid batchId)
{ {
var model = await storyIntelligence.GetCharacterReviewAsync(batchId, page); var model = await storyIntelligence.GetProgressAsync(batchId);
if (model is null) if (model is null)
{ {
return NotFound(); return NotFound();
@ -398,21 +180,18 @@ public sealed class OnboardingController(
if (model.HasActiveRuns) if (model.HasActiveRuns)
{ {
return RedirectToBook(model); return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
} }
if (!model.SceneAnalysisComplete) return !model.AllCommitted
{ ? RedirectToAction(nameof(StoryIntelligenceReview), new { batchId })
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId }); : View(model);
}
return View(model);
} }
[HttpGet("story-intelligence/locations")] [HttpGet("story-intelligence/locations")]
public async Task<IActionResult> StoryIntelligenceLocations(Guid batchId, int page = 1, string scope = "significant") public async Task<IActionResult> StoryIntelligenceLocations(Guid batchId)
{ {
var model = await storyIntelligence.GetLocationReviewAsync(batchId, page, scope: scope); var model = await storyIntelligence.GetProgressAsync(batchId);
if (model is null) if (model is null)
{ {
return NotFound(); return NotFound();
@ -420,23 +199,23 @@ public sealed class OnboardingController(
if (model.HasActiveRuns) if (model.HasActiveRuns)
{ {
return RedirectToBook(model); return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
} }
if (!model.SceneAnalysisComplete) if (!model.AllCommitted)
{ {
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId }); return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
} }
return !model.CharacterGateSatisfied return !model.PipelineDashboard.CharacterStageComplete
? RedirectToReviewCentreWithCharacterGate(batchId) ? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId })
: View(model); : View(model);
} }
[HttpGet("story-intelligence/assets")] [HttpGet("story-intelligence/assets")]
public async Task<IActionResult> StoryIntelligenceAssets(Guid batchId, int page = 1) public async Task<IActionResult> StoryIntelligenceAssets(Guid batchId)
{ {
var model = await storyIntelligence.GetAssetReviewAsync(batchId, page); var model = await storyIntelligence.GetProgressAsync(batchId);
if (model is null) if (model is null)
{ {
return NotFound(); return NotFound();
@ -444,26 +223,28 @@ public sealed class OnboardingController(
if (model.HasActiveRuns) if (model.HasActiveRuns)
{ {
return RedirectToBook(model); return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
} }
if (!model.SceneAnalysisComplete) if (!model.AllCommitted)
{ {
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId }); return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
} }
if (!model.CharacterGateSatisfied) if (!model.PipelineDashboard.CharacterStageComplete)
{ {
return RedirectToReviewCentreWithCharacterGate(batchId); return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
} }
return View(model); return !model.PipelineDashboard.LocationStageComplete
? RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId })
: View(model);
} }
[HttpGet("story-intelligence/relationships")] [HttpGet("story-intelligence/relationships")]
public async Task<IActionResult> StoryIntelligenceRelationships(Guid batchId, int page = 1) public async Task<IActionResult> StoryIntelligenceRelationships(Guid batchId)
{ {
var model = await storyIntelligence.GetRelationshipReviewAsync(batchId, page); var model = await storyIntelligence.GetProgressAsync(batchId);
if (model is null) if (model is null)
{ {
return NotFound(); return NotFound();
@ -471,20 +252,27 @@ public sealed class OnboardingController(
if (model.HasActiveRuns) if (model.HasActiveRuns)
{ {
return RedirectToBook(model); return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
} }
if (!model.SceneAnalysisComplete) if (!model.AllCommitted)
{ {
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId }); return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
} }
if (!model.CharacterGateSatisfied) if (!model.PipelineDashboard.CharacterStageComplete)
{ {
return RedirectToReviewCentreWithCharacterGate(batchId); return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
} }
return View(model); if (!model.PipelineDashboard.LocationStageComplete)
{
return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId });
}
return !model.PipelineDashboard.AssetStageComplete
? RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId })
: View(model);
} }
[HttpPost("story-intelligence/cancel")] [HttpPost("story-intelligence/cancel")]
@ -498,7 +286,7 @@ public sealed class OnboardingController(
} }
TempData["OnboardingStoryIntelligenceMessage"] = "Analysis cancellation requested."; TempData["OnboardingStoryIntelligenceMessage"] = "Analysis cancellation requested.";
return RedirectToBook(progress); return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
} }
[HttpPost("story-intelligence/commit")] [HttpPost("story-intelligence/commit")]
@ -512,7 +300,13 @@ public sealed class OnboardingController(
} }
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId }); if (result.Success && progress.AllCommitted)
{
TempData["WriterWorkspaceMessage"] = $"{progress.CommittedChapterCount} chapter(s) and {progress.Chapters.Sum(chapter => chapter.ScenesCreated)} scene(s) created from your manuscript.";
return RedirectToAction("Index", "Writer", new { projectId = progress.ProjectID });
}
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
} }
[HttpPost("story-intelligence/commit-all")] [HttpPost("story-intelligence/commit-all")]
@ -526,7 +320,9 @@ public sealed class OnboardingController(
} }
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId }); return result.Success && progress.AllCommitted
? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId })
: RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
} }
[HttpPost("story-intelligence/characters")] [HttpPost("story-intelligence/characters")]
@ -541,13 +337,20 @@ public sealed class OnboardingController(
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
return result.Success return result.Success
? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID }) ? RedirectToAction(nameof(StoryIntelligenceCharacterComplete), new { batchId = form.BatchID })
: RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId = form.BatchID }); : RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId = form.BatchID });
} }
[HttpGet("story-intelligence/characters/complete")]
public async Task<IActionResult> StoryIntelligenceCharacterComplete(Guid batchId)
{
var model = await storyIntelligence.GetCharacterImportResultAsync(batchId);
return model is null ? NotFound() : View(model);
}
[HttpPost("story-intelligence/locations")] [HttpPost("story-intelligence/locations")]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> ImportStoryIntelligenceLocations(StoryIntelligenceLocationImportForm form, string intent = "save") public async Task<IActionResult> ImportStoryIntelligenceLocations(StoryIntelligenceLocationImportForm form)
{ {
var (progress, result) = await storyIntelligence.ImportLocationsAsync(form.BatchID, form); var (progress, result) = await storyIntelligence.ImportLocationsAsync(form.BatchID, form);
if (progress is null) if (progress is null)
@ -556,31 +359,21 @@ public sealed class OnboardingController(
} }
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
if (!result.Success) return result.Success
{ ? RedirectToAction(nameof(StoryIntelligenceLocationComplete), new { batchId = form.BatchID })
return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = form.BatchID, page = Math.Max(1, form.PageNumber), scope = form.Scope }); : RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = form.BatchID });
} }
var pendingCount = progress.LocationReview.PendingCandidateCount; [HttpGet("story-intelligence/locations/complete")]
if (pendingCount <= 0) public async Task<IActionResult> StoryIntelligenceLocationComplete(Guid batchId)
{ {
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID }); var model = await storyIntelligence.GetLocationImportResultAsync(batchId);
} return model is null ? NotFound() : View(model);
const int pageSize = 25;
var pageCount = Math.Max(1, (int)Math.Ceiling(pendingCount / (double)pageSize));
var currentPage = Math.Max(1, form.PageNumber);
var targetPage = string.Equals(intent, "previous", StringComparison.OrdinalIgnoreCase)
? currentPage - 1
: currentPage;
targetPage = Math.Clamp(targetPage, 1, pageCount);
return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = form.BatchID, page = targetPage, scope = form.Scope });
} }
[HttpPost("story-intelligence/assets")] [HttpPost("story-intelligence/assets")]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> ImportStoryIntelligenceAssets(StoryIntelligenceAssetImportForm form, string intent = "save") public async Task<IActionResult> ImportStoryIntelligenceAssets(StoryIntelligenceAssetImportForm form)
{ {
var (progress, result) = await storyIntelligence.ImportAssetsAsync(form.BatchID, form); var (progress, result) = await storyIntelligence.ImportAssetsAsync(form.BatchID, form);
if (progress is null) if (progress is null)
@ -589,37 +382,22 @@ public sealed class OnboardingController(
} }
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
if (!result.Success) return result.Success
{ ? RedirectToAction(nameof(StoryIntelligenceAssetComplete), new { batchId = form.BatchID })
return RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId = form.BatchID, page = Math.Max(1, form.PageNumber) }); : RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId = form.BatchID });
} }
var pendingCount = progress.AssetReview.SummaryNeedsAttentionCount ?? progress.AssetReview.PendingCandidateCount; [HttpGet("story-intelligence/assets/complete")]
if (pendingCount <= 0) public async Task<IActionResult> StoryIntelligenceAssetComplete(Guid batchId)
{ {
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID }); var model = await storyIntelligence.GetAssetImportResultAsync(batchId);
} return model is null ? NotFound() : View(model);
const int pageSize = 25;
var pageCount = Math.Max(1, (int)Math.Ceiling(pendingCount / (double)pageSize));
var currentPage = Math.Max(1, form.PageNumber);
var targetPage = string.Equals(intent, "previous", StringComparison.OrdinalIgnoreCase)
? currentPage - 1
: currentPage;
targetPage = Math.Clamp(targetPage, 1, pageCount);
return RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId = form.BatchID, page = targetPage });
} }
[HttpPost("story-intelligence/relationships")] [HttpPost("story-intelligence/relationships")]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> ImportStoryIntelligenceRelationships(StoryIntelligenceRelationshipImportForm form) public async Task<IActionResult> ImportStoryIntelligenceRelationships(StoryIntelligenceRelationshipImportForm form)
{ {
if (!ModelState.IsValid)
{
TempData["OnboardingStoryIntelligenceError"] = "Check relationship values before saving. Intensity must be a whole number from 1 to 10, or blank.";
return RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId = form.BatchID });
}
var (progress, result) = await storyIntelligence.ImportRelationshipsAsync(form.BatchID, form); var (progress, result) = await storyIntelligence.ImportRelationshipsAsync(form.BatchID, form);
if (progress is null) if (progress is null)
{ {
@ -628,14 +406,21 @@ public sealed class OnboardingController(
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
return result.Success return result.Success
? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID }) ? RedirectToAction(nameof(StoryIntelligenceRelationshipComplete), new { batchId = form.BatchID })
: RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId = form.BatchID }); : RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId = form.BatchID });
} }
[HttpGet("story-intelligence/knowledge")] [HttpGet("story-intelligence/relationships/complete")]
public async Task<IActionResult> StoryIntelligenceKnowledge(Guid batchId, int page = 1) public async Task<IActionResult> StoryIntelligenceRelationshipComplete(Guid batchId)
{ {
var model = await storyIntelligence.GetKnowledgeReviewAsync(batchId, page); var model = await storyIntelligence.GetRelationshipImportResultAsync(batchId);
return model is null ? NotFound() : View(model);
}
[HttpGet("story-intelligence/knowledge")]
public async Task<IActionResult> StoryIntelligenceKnowledge(Guid batchId)
{
var model = await storyIntelligence.GetProgressAsync(batchId);
if (model is null) if (model is null)
{ {
return NotFound(); return NotFound();
@ -643,25 +428,37 @@ public sealed class OnboardingController(
if (model.HasActiveRuns) if (model.HasActiveRuns)
{ {
return RedirectToBook(model); return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
} }
if (!model.SceneAnalysisComplete) if (!model.AllCommitted)
{ {
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId }); return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
} }
if (!model.CharacterGateSatisfied) if (!model.PipelineDashboard.CharacterStageComplete)
{ {
return RedirectToReviewCentreWithCharacterGate(batchId); return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
} }
return View(model); if (!model.PipelineDashboard.LocationStageComplete)
{
return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId });
}
if (!model.PipelineDashboard.AssetStageComplete)
{
return RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId });
}
return !model.PipelineDashboard.RelationshipStageComplete
? RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId })
: View(model);
} }
[HttpPost("story-intelligence/knowledge")] [HttpPost("story-intelligence/knowledge")]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> ImportStoryIntelligenceKnowledge(StoryIntelligenceKnowledgeImportForm form, string intent = "save") public async Task<IActionResult> ImportStoryIntelligenceKnowledge(StoryIntelligenceKnowledgeImportForm form)
{ {
var (progress, result) = await storyIntelligence.ImportKnowledgeAsync(form.BatchID, form); var (progress, result) = await storyIntelligence.ImportKnowledgeAsync(form.BatchID, form);
if (progress is null) if (progress is null)
@ -670,119 +467,54 @@ public sealed class OnboardingController(
} }
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
if (!result.Success) return result.Success
{ ? RedirectToAction(nameof(StoryIntelligenceKnowledgeComplete), new { batchId = form.BatchID })
return RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = form.BatchID, page = Math.Max(1, form.PageNumber) }); : RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = form.BatchID });
}
var pendingCount = progress.KnowledgeReview.SummaryNeedsAttentionCount ?? progress.KnowledgeReview.PendingCandidateCount;
if (pendingCount <= 0)
{
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID });
}
const int pageSize = 25;
var pageCount = Math.Max(1, (int)Math.Ceiling(pendingCount / (double)pageSize));
var currentPage = Math.Max(1, form.PageNumber);
var targetPage = string.Equals(intent, "previous", StringComparison.OrdinalIgnoreCase)
? currentPage - 1
: currentPage;
targetPage = Math.Clamp(targetPage, 1, pageCount);
return RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = form.BatchID, page = targetPage });
} }
[HttpGet("story-intelligence/narrative")] [HttpGet("story-intelligence/knowledge/complete")]
public async Task<IActionResult> StoryIntelligenceNarrative(Guid batchId) public async Task<IActionResult> StoryIntelligenceKnowledgeComplete(Guid batchId)
{ {
var model = await storyIntelligence.GetNarrativeReviewAsync(batchId); var model = await storyIntelligence.GetKnowledgeImportResultAsync(batchId);
if (model is null) return model is null ? NotFound() : View(model);
{
return NotFound();
}
if (model.HasActiveRuns)
{
return RedirectToBook(model);
}
if (!model.SceneAnalysisComplete)
{
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
}
if (!model.CharacterGateSatisfied)
{
return RedirectToReviewCentreWithCharacterGate(batchId);
}
return View(model);
} }
[HttpPost("story-intelligence/narrative")] [HttpGet("story-intelligence/complete")]
[ValidateAntiForgeryToken] public async Task<IActionResult> StoryIntelligenceComplete(Guid batchId)
public async Task<IActionResult> ImportStoryIntelligenceNarrative(StoryIntelligenceNarrativeImportForm form)
{ {
var (progress, result) = await storyIntelligence.ImportNarrativeAsync(form.BatchID, form); var progress = await storyIntelligence.GetProgressAsync(batchId);
if (progress is null) if (progress is null)
{ {
return NotFound(); return NotFound();
} }
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message; if (!progress.PipelineDashboard.CharacterStageComplete)
return result.Success
? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID })
: RedirectToAction(nameof(StoryIntelligenceNarrative), new { batchId = form.BatchID });
}
[HttpPost("story-intelligence/plot-synthesis/retry")]
public async Task<IActionResult> RetryStoryIntelligencePlotSynthesis(Guid batchId)
{
var model = await storyIntelligence.GetReviewCentreAsync(batchId);
if (model is null)
{ {
return NotFound(); return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
} }
if (!currentUser.UserId.HasValue) if (!progress.PipelineDashboard.LocationStageComplete)
{ {
return Forbid(); return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId });
} }
await wholeBookPlotIntelligence.RetryAsync(model.BookID, currentUser.UserId.Value); if (!progress.PipelineDashboard.AssetStageComplete)
TempData["OnboardingStoryIntelligenceMessage"] = "Plot Lines analysis has been queued again.";
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
}
[HttpPost("story-intelligence/character-enrichment/retry")]
public async Task<IActionResult> RetryStoryIntelligenceCharacterEnrichment(Guid batchId)
{
var model = await storyIntelligence.GetReviewCentreAsync(batchId);
if (model is null)
{ {
return NotFound(); return RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId });
} }
if (!currentUser.UserId.HasValue) if (!progress.PipelineDashboard.RelationshipStageComplete)
{ {
return Forbid(); return RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId });
} }
await characterEnrichment.RetryAsync(model.BookID, currentUser.UserId.Value); if (!progress.PipelineDashboard.KnowledgeStageComplete)
TempData["OnboardingStoryIntelligenceMessage"] = "Character enrichment has been queued again."; {
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId }); return RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId });
} }
private IActionResult RedirectToBook(StoryIntelligenceProgressViewModel model) var model = await storyIntelligence.GetCompletionAsync(batchId);
=> RedirectToAction("Details", "Books", new { id = model.BookID }); return model is null ? NotFound() : View(model);
private IActionResult RedirectToBook(StoryIntelligenceOverviewViewModel model)
=> RedirectToAction("Details", "Books", new { id = model.BookID });
private IActionResult RedirectToReviewCentreWithCharacterGate(Guid batchId)
{
TempData["OnboardingStoryIntelligenceMessage"] = "Resolve Characters before reviewing this category.";
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
} }
[HttpPost("scan-review")] [HttpPost("scan-review")]
@ -794,54 +526,25 @@ public sealed class OnboardingController(
var continueToCharacters = string.Equals(intent, "characters", StringComparison.OrdinalIgnoreCase); var continueToCharacters = string.Equals(intent, "characters", StringComparison.OrdinalIgnoreCase);
var continueCharacterReview = string.Equals(intent, "character-next", StringComparison.OrdinalIgnoreCase); var continueCharacterReview = string.Equals(intent, "character-next", StringComparison.OrdinalIgnoreCase);
var backCharacterReview = string.Equals(intent, "character-back", StringComparison.OrdinalIgnoreCase); var backCharacterReview = string.Equals(intent, "character-back", StringComparison.OrdinalIgnoreCase);
var readyToImport = string.Equals(intent, "structure", StringComparison.OrdinalIgnoreCase); var readyToImport = string.Equals(intent, "continue", StringComparison.OrdinalIgnoreCase);
var completeCoreImport = string.Equals(intent, "continue", StringComparison.OrdinalIgnoreCase);
var nextCharacterPage = Math.Max(1, characterPage + (backCharacterReview ? -1 : 1)); var nextCharacterPage = Math.Max(1, characterPage + (backCharacterReview ? -1 : 1));
try try
{ {
if (completeCoreImport)
{
var result = await onboarding.FinaliseCharacterReviewAsync(form);
if (result is null)
{
return NotFound();
}
TempData["OnboardingReviewMessage"] = "Characters saved. Core manuscript import is complete.";
return RedirectToAction(nameof(BuildComplete), new { previewId = form.PreviewID });
}
var model = await onboarding.SaveScanReviewAsync(form, readyToImport); var model = await onboarding.SaveScanReviewAsync(form, readyToImport);
if (model is null) if (model is null)
{ {
return NotFound(); return NotFound();
} }
if (continueToCharacters)
{
var build = await onboarding.GetBuildResultAsync(form.PreviewID);
if (build is null || build.SceneMappings.Count == 0)
{
TempData["ArchiveError"] = "Import the approved chapters and scenes before reviewing characters.";
await onboarding.SetCurrentStepAsync(OnboardingSteps.NextPathPreview);
return RedirectToAction(nameof(Index));
}
if (!await onboarding.IsBasicLocationReviewCompleteAsync(form.PreviewID))
{
TempData["OnboardingReviewMessage"] = "Review or skip basic locations before reviewing characters.";
return RedirectToAction(nameof(ScanLocations), new { previewId = form.PreviewID });
}
}
TempData["OnboardingReviewMessage"] = readyToImport TempData["OnboardingReviewMessage"] = readyToImport
? "Chapter review saved. Next: import scenes and update Word." ? "Review saved. Next: story and portrait context."
: continueToCharacters ? "Chapter and scene review saved. Next: review detected characters." : continueToCharacters ? "Chapter and scene review saved. Next: review detected characters."
: continueCharacterReview || backCharacterReview ? "Character review choices saved." : continueCharacterReview || backCharacterReview ? "Character review choices saved."
: "Review choices saved."; : "Review choices saved.";
return readyToImport return readyToImport
? RedirectToAction(nameof(Index)) ? RedirectToAction(nameof(StoryIntelligence))
: continueCharacterReview || backCharacterReview ? RedirectToAction(nameof(ScanCharacters), new { previewId = form.PreviewID, page = nextCharacterPage }) : continueCharacterReview || backCharacterReview ? RedirectToAction(nameof(ScanCharacters), new { previewId = form.PreviewID, page = nextCharacterPage })
: continueToCharacters ? RedirectToAction(nameof(ScanLocations), new { previewId = form.PreviewID }) : continueToCharacters ? RedirectToAction(nameof(ScanCharacters), new { previewId = form.PreviewID })
: RedirectToAction(nameof(ScanReview), new { previewId = form.PreviewID }); : RedirectToAction(nameof(ScanReview), new { previewId = form.PreviewID });
} }
catch (InvalidOperationException ex) catch (InvalidOperationException ex)
@ -853,7 +556,7 @@ public sealed class OnboardingController(
return NotFound(); return NotFound();
} }
if (continueToCharacters || continueCharacterReview || backCharacterReview || completeCoreImport) if (readyToImport || continueCharacterReview || backCharacterReview)
{ {
ViewData["ScanReviewStep"] = "characters"; ViewData["ScanReviewStep"] = "characters";
ViewData["CharacterReviewPage"] = readyToImport ViewData["CharacterReviewPage"] = readyToImport

View File

@ -50,10 +50,8 @@ public sealed class ProjectMetricsController(ISceneMetricTypeService metrics) :
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> AddMissingDefaults(int projectId) public async Task<IActionResult> AddMissingDefaults(int projectId)
{ {
var backfilled = await metrics.AddMissingDefaultsAsync(projectId); await metrics.AddMissingDefaultsAsync(projectId);
TempData["ProjectMetricMessage"] = backfilled > 0 TempData["ProjectMetricMessage"] = "Missing default scene metrics added.";
? $"Missing default scene metrics added. {backfilled:N0} scene metric value(s) backfilled."
: "Missing default scene metrics added. Existing scenes already had values for the enabled defaults.";
return RedirectToAction(nameof(Index), new { projectId }); return RedirectToAction(nameof(Index), new { projectId });
} }

View File

@ -6,7 +6,7 @@ using PlotLine.ViewModels;
namespace PlotLine.Controllers; namespace PlotLine.Controllers;
[Authorize] [Authorize]
public sealed class ProjectsController(IProjectService projects, IProjectActivityService activity, ILogger<ProjectsController> logger) : Controller public sealed class ProjectsController(IProjectService projects, IProjectActivityService activity) : Controller
{ {
public async Task<IActionResult> Index() => View(await projects.ListAsync()); public async Task<IActionResult> Index() => View(await projects.ListAsync());
@ -92,19 +92,15 @@ public sealed class ProjectsController(IProjectService projects, IProjectActivit
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> HardDeleteArchivedProject(int projectId, string? confirmationName) public async Task<IActionResult> HardDeleteArchivedProject(int projectId, string? confirmationName)
{ {
logger.LogWarning("Permanent project delete requested for archived project {ProjectID}.", projectId);
var project = await projects.GetArchivedOwnerProjectAsync(projectId); var project = await projects.GetArchivedOwnerProjectAsync(projectId);
if (project is null) if (project is null)
{ {
logger.LogWarning("Permanent project delete rejected for project {ProjectID}: project is not archived or current user is not owner.", projectId);
TempData["ArchiveError"] = "Only an archived project owner can permanently delete a project."; TempData["ArchiveError"] = "Only an archived project owner can permanently delete a project.";
return RedirectToAction(nameof(Index)); return RedirectToAction(nameof(Index));
} }
if (!string.Equals(confirmationName?.Trim(), project.ProjectName, StringComparison.Ordinal)) if (!string.Equals(confirmationName?.Trim(), project.ProjectName, StringComparison.Ordinal))
{ {
logger.LogWarning("Permanent project delete rejected for archived project {ProjectID}: confirmation name did not match.", projectId);
TempData["ArchiveError"] = "Type the project name exactly to permanently delete it."; TempData["ArchiveError"] = "Type the project name exactly to permanently delete it.";
return RedirectToAction(nameof(Index)); return RedirectToAction(nameof(Index));
} }
@ -112,12 +108,10 @@ public sealed class ProjectsController(IProjectService projects, IProjectActivit
var result = await projects.HardDeleteAsync(projectId); var result = await projects.HardDeleteAsync(projectId);
if (result.Succeeded) if (result.Succeeded)
{ {
logger.LogWarning("Permanent project delete completed for archived project {ProjectID}.", projectId);
TempData["ArchiveMessage"] = "Project permanently deleted."; TempData["ArchiveMessage"] = "Project permanently deleted.";
} }
else else
{ {
logger.LogWarning("Permanent project delete failed for archived project {ProjectID}: {Message}", projectId, result.Message);
TempData["ArchiveError"] = result.NotArchived TempData["ArchiveError"] = result.NotArchived
? "Only archived projects can be permanently deleted." ? "Only archived projects can be permanently deleted."
: result.Message; : result.Message;

View File

@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using PlotLine.Models;
using PlotLine.Services; using PlotLine.Services;
namespace PlotLine.Controllers; namespace PlotLine.Controllers;
@ -10,10 +9,7 @@ public sealed class RelationshipMapController(IRelationshipMapService relationsh
{ {
public async Task<IActionResult> Index(int projectId, int? bookId, int? chapterId, int? sceneId, int? focusCharacterId, bool showFullNetwork = false) public async Task<IActionResult> Index(int projectId, int? bookId, int? chapterId, int? sceneId, int? focusCharacterId, bool showFullNetwork = false)
{ {
var position = StoryPosition.FromTimelineSelection(projectId, bookId, chapterId, sceneId); var model = await relationshipMap.GetAsync(projectId, bookId, chapterId, sceneId, focusCharacterId, showFullNetwork);
var model = position is null
? null
: await relationshipMap.GetAsync(position, focusCharacterId, showFullNetwork);
return model is null ? NotFound() : View(model); return model is null ? NotFound() : View(model);
} }
} }

View File

@ -32,36 +32,6 @@ public sealed class StoryAssetsController(IAssetService assets) : Controller
return model is null ? NotFound() : View(model); return model is null ? NotFound() : View(model);
} }
public async Task<IActionResult> Merge(int id)
{
var model = await assets.GetMergeAsync(id);
return model is null ? NotFound() : View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Merge([Bind(Prefix = "Form")] EntityMergeForm form)
{
if (ModelState.IsValid)
{
try
{
var result = await assets.MergeAsync(form);
if (result is null) return NotFound();
TempData["AssetMessage"] = $"{result.SourceName} merged into {result.TargetName}.";
return RedirectToAction(nameof(Details), new { id = result.TargetID });
}
catch (InvalidOperationException ex)
{
ModelState.AddModelError(string.Empty, ex.Message);
}
}
var model = await assets.GetMergeAsync(form.SourceID);
if (model is null) return NotFound();
model.Form = form;
return View(model);
}
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> Save(StoryAssetEditViewModel model) public async Task<IActionResult> Save(StoryAssetEditViewModel model)

View File

@ -1,110 +0,0 @@
using System.Globalization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using PlotLine.Services;
namespace PlotLine.Controllers;
[Authorize]
public sealed class StoryCalendarController(IStoryCalendarService calendar, ILogger<StoryCalendarController> logger) : Controller
{
public async Task<IActionResult> Index(int projectId, int? bookId, string? month, string? date)
{
if (projectId == 0)
{
return BadRequest("ProjectID is required.");
}
var selectedMonth = ParseMonth(month);
var selectedDate = ParseDate(date);
var model = await calendar.GetAsync(projectId, bookId, selectedMonth, selectedDate);
if (model is null)
{
return NotFound();
}
return IsAjaxRequest()
? PartialView("_StoryCalendarSurface", model)
: View(model);
}
[HttpGet]
public async Task<IActionResult> DayDetails(int projectId, int? bookId, string? date)
{
if (projectId == 0)
{
return BadRequest("ProjectID is required.");
}
var selectedDate = ParseDate(date);
if (!selectedDate.HasValue)
{
return BadRequest("Date is required.");
}
try
{
var model = await calendar.GetAsync(projectId, bookId, FirstOfMonth(selectedDate.Value), selectedDate);
return model is null ? NotFound() : PartialView("_StoryCalendarDayDetails", model.DayDetails);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to load Story Calendar day details for project {ProjectID} and date {StoryDate}.", projectId, date);
return StatusCode(StatusCodes.Status500InternalServerError, "The selected story date could not be loaded.");
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult ApplyFilters(int projectId, int? bookId, string? month)
{
return RedirectToAction(nameof(Index), new
{
projectId,
bookId,
month
});
}
private static DateOnly? ParseMonth(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return DateOnly.TryParseExact(
$"{value.Trim()}-01",
"yyyy-MM-dd",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var parsed)
? new DateOnly(parsed.Year, parsed.Month, 1)
: null;
}
private static DateOnly? ParseDate(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return DateOnly.TryParseExact(
value.Trim(),
"yyyy-MM-dd",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var parsed)
? parsed
: null;
}
private static DateOnly FirstOfMonth(DateOnly date)
=> new(date.Year, date.Month, 1);
private bool IsAjaxRequest()
=> string.Equals(Request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.OrdinalIgnoreCase);
}

View File

@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
namespace PlotLine.Controllers;
[Authorize]
[Route("api/story-intelligence")]
public sealed class StoryIntelligenceVisualisationController(
ICurrentUserService currentUser,
IStoryIntelligenceVisualisationSnapshotService snapshots,
ILogger<StoryIntelligenceVisualisationController> logger) : ControllerBase
{
[HttpGet("runs/{runId:int}/visualisation-snapshot")]
public async Task<IActionResult> GetSnapshot(int runId)
{
if (currentUser.UserId is not int userId)
{
return Unauthorized();
}
var snapshot = await snapshots.BuildSnapshotAsync(runId, userId);
if (snapshot is not null)
{
var current = snapshot.Scenes.FirstOrDefault(scene => scene.IsCurrent) ?? snapshot.Scenes.LastOrDefault();
logger.LogInformation(
"Story Intelligence browser endpoint synchronisation: import session {ImportSessionId}; run {RunId}; current chapter {CurrentChapter}; current scene {CurrentScene}; scene result {SceneResultId}; snapshot version Endpoint; change token {ChangeToken}.",
snapshot.ImportSessionId,
snapshot.RunId,
current?.ChapterLabel,
current?.SceneNumber,
snapshot.Diagnostics.SelectedCurrentSceneResultId,
snapshot.ChangeToken);
}
return snapshot is null ? NotFound() : Ok(snapshot);
}
[HttpGet("import-sessions/{importSessionId:int}/visualisation-snapshot")]
public async Task<IActionResult> GetImportSessionSnapshot(int importSessionId)
{
if (currentUser.UserId is not int userId)
{
return Unauthorized();
}
var snapshot = await snapshots.BuildImportSessionSnapshotAsync(importSessionId, userId);
if (snapshot is not null)
{
var current = snapshot.Scenes.FirstOrDefault(scene => scene.IsCurrent) ?? snapshot.Scenes.LastOrDefault();
logger.LogInformation(
"Story Intelligence browser endpoint synchronisation: import session {ImportSessionId}; run {RunId}; current chapter {CurrentChapter}; current scene {CurrentScene}; scene result {SceneResultId}; snapshot version Endpoint; change token {ChangeToken}.",
snapshot.ImportSessionId,
snapshot.RunId,
current?.ChapterLabel,
current?.SceneNumber,
snapshot.Diagnostics.SelectedCurrentSceneResultId,
snapshot.ChangeToken);
}
return snapshot is null ? NotFound() : Ok(snapshot);
}
}

View File

@ -1,20 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
namespace PlotLine.Controllers;
[Authorize]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public sealed class StoryPointController(IStoryPointContextService context) : Controller
{
[HttpGet]
public async Task<IActionResult> Index(int projectId, int? sceneId)
{
if (projectId <= 0) return BadRequest("ProjectID is required.");
var model = await context.GetAsync(projectId, sceneId);
if (model is null) return NotFound();
return string.Equals(Request.Headers["X-Requested-With"], "XMLHttpRequest", StringComparison.OrdinalIgnoreCase)
? PartialView("_Snapshot", model.Snapshot) : View(model);
}
}

View File

@ -13,9 +13,7 @@ namespace PlotLine.Controllers;
[Route("api/word-companion")] [Route("api/word-companion")]
public sealed class WordCompanionController( public sealed class WordCompanionController(
IWordCompanionService wordCompanion, IWordCompanionService wordCompanion,
IWordCompanionPresenceService presence, IHubContext<WordCompanionFollowHub> followHub) : ControllerBase
IHubContext<WordCompanionFollowHub> followHub,
ILogger<WordCompanionController> logger) : ControllerBase
{ {
[HttpGet("projects")] [HttpGet("projects")]
public Task<WordCompanionProjectsResponse> GetProjects() => wordCompanion.ListProjectsAsync(); public Task<WordCompanionProjectsResponse> GetProjects() => wordCompanion.ListProjectsAsync();
@ -126,54 +124,19 @@ public sealed class WordCompanionController(
[HttpPost("runtime/current-scene")] [HttpPost("runtime/current-scene")]
public async Task<IActionResult> UpdateRuntimeCurrentScene(WordCompanionRuntimeCurrentSceneRequest request) public async Task<IActionResult> UpdateRuntimeCurrentScene(WordCompanionRuntimeCurrentSceneRequest request)
{ {
logger.LogInformation(
"Word Companion current scene received. ProjectID={ProjectID} BookID={BookID} ChapterID={ChapterID} SceneID={SceneID} DocumentGuidPresent={DocumentGuidPresent}",
request.ProjectId,
request.BookId,
request.ChapterId,
request.SceneId,
request.DocumentGuid != Guid.Empty);
var response = await wordCompanion.ValidateRuntimeCurrentSceneAsync(request); var response = await wordCompanion.ValidateRuntimeCurrentSceneAsync(request);
if (response is null) if (response is null)
{ {
logger.LogWarning(
"Word Companion current scene rejected. ProjectID={ProjectID} BookID={BookID} ChapterID={ChapterID} SceneID={SceneID} DocumentGuidPresent={DocumentGuidPresent}",
request.ProjectId,
request.BookId,
request.ChapterId,
request.SceneId,
request.DocumentGuid != Guid.Empty);
return BadRequest(); return BadRequest();
} }
await presence.UpdateCurrentStoryPositionAsync(RequireUserId(), response.Position);
logger.LogInformation(
"Word Companion current scene accepted. ProjectID={ProjectID} BookID={BookID} ChapterID={ChapterID} SceneID={SceneID}",
response.ProjectId,
response.BookId,
response.ChapterId,
response.SceneId);
await BroadcastToCurrentUserAsync("wordCompanionSceneChanged", new await BroadcastToCurrentUserAsync("wordCompanionSceneChanged", new
{ {
response.ProjectId, response.ProjectId,
response.BookId, response.BookId,
response.ChapterId, response.ChapterId,
response.SceneId, response.SceneId,
response.SceneUrl, response.SceneUrl
response.Position
});
await BroadcastToCurrentUserAsync("storyPositionChanged", new
{
Source = "WordCompanion",
response.ProjectId,
response.BookId,
response.ChapterId,
response.SceneId,
CanonicalUrl = response.SceneUrl,
response.Position
}); });
return Ok(response); return Ok(response);
@ -292,12 +255,4 @@ public sealed class WordCompanionController(
await followHub.Clients.User(userId).SendAsync(eventName, payload); await followHub.Clients.User(userId).SendAsync(eventName, payload);
} }
} }
private int RequireUserId()
{
var raw = User.FindFirstValue(ClaimTypes.NameIdentifier);
return int.TryParse(raw, out var userId) && userId > 0
? userId
: throw new InvalidOperationException("The current user could not be identified.");
}
} }

View File

@ -7,21 +7,13 @@ namespace PlotLine.Controllers;
[AllowAnonymous] [AllowAnonymous]
[Route("word-companion")] [Route("word-companion")]
public sealed class WordCompanionHostController( public sealed class WordCompanionHostController(ICurrentUserService currentUser, IAuthorizationService authorization) : Controller
ICurrentUserService currentUser,
IAuthorizationService authorization,
ILogger<WordCompanionHostController> logger) : Controller
{ {
[HttpGet("")] [HttpGet("")]
public async Task<IActionResult> Index() public async Task<IActionResult> Index()
{ {
var isAdmin = currentUser.IsAuthenticated var isAdmin = currentUser.IsAuthenticated
&& (await authorization.AuthorizeAsync(User, "AdminOnly")).Succeeded; && (await authorization.AuthorizeAsync(User, "AdminOnly")).Succeeded;
logger.LogInformation(
"Serving Word Companion host. IsAuthenticated={IsAuthenticated} UserID={UserID} IsAdmin={IsAdmin}",
currentUser.IsAuthenticated,
currentUser.UserId,
isAdmin);
return View(new WordCompanionHostViewModel return View(new WordCompanionHostViewModel
{ {
@ -33,3 +25,4 @@ public sealed class WordCompanionHostController(
}); });
} }
} }

View File

@ -1,256 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using PlotLine.Models;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
[Authorize]
[Route("Writing")]
public sealed class WritingController(
IStoryContextService storyContext,
IRelationshipMapService relationshipMap,
IFloorPlanService floorPlans,
IWriterWorkspaceService writerWorkspace,
ILogger<WritingController> logger) : Controller
{
[HttpGet("Context")]
public async Task<IActionResult> Context(int? projectId, int? bookId, int? chapterId, int? sceneId, Guid? documentGuid)
{
logger.LogInformation(
"Opening Writing Context. ProjectID={ProjectID} BookID={BookID} ChapterID={ChapterID} SceneID={SceneID} DocumentGuidPresent={DocumentGuidPresent}",
projectId,
bookId,
chapterId,
sceneId,
documentGuid.HasValue);
var display = sceneId.HasValue
? await BuildDisplayAsync(projectId, bookId, chapterId, sceneId, "Manual context")
: WaitingContextDisplay(projectId, bookId, documentGuid);
var model = new WritingContextPageViewModel
{
ProjectID = projectId,
BookID = bookId,
ChapterID = chapterId,
SceneID = sceneId,
DocumentGuid = documentGuid,
Display = display ?? MissingContextDisplay(projectId, bookId, sceneId)
};
return View(model);
}
[HttpGet("Intent")]
public async Task<IActionResult> Intent(int? projectId, int? bookId, int? chapterId, int? sceneId, Guid? documentGuid)
{
logger.LogInformation(
"Opening Writing Intent. ProjectID={ProjectID} BookID={BookID} ChapterID={ChapterID} SceneID={SceneID} DocumentGuidPresent={DocumentGuidPresent}",
projectId,
bookId,
chapterId,
sceneId,
documentGuid.HasValue);
var display = sceneId.HasValue || chapterId.HasValue
? await BuildIntentDisplayAsync(projectId, bookId, chapterId, sceneId, "Manual intent")
: WaitingIntentDisplay(projectId, bookId, documentGuid);
var model = new WritingIntentPageViewModel
{
ProjectID = projectId,
BookID = bookId,
ChapterID = chapterId,
SceneID = sceneId,
DocumentGuid = documentGuid,
Display = display ?? MissingIntentDisplay(projectId, bookId, chapterId, sceneId)
};
return View(model);
}
[HttpGet("Context/Scene")]
public async Task<IActionResult> ContextScene(int? projectId, int? bookId, int? chapterId, int? sceneId)
{
if (!sceneId.HasValue)
{
logger.LogWarning("Writing Context partial requested without SceneID. ProjectID={ProjectID} BookID={BookID} ChapterID={ChapterID}", projectId, bookId, chapterId);
return PartialView("~/Views/Writing/_WritingContextContent.cshtml", WaitingContextDisplay(projectId, bookId, null));
}
var model = await BuildDisplayAsync(projectId, bookId, chapterId, sceneId, "Following Word");
if (model?.HasScene == true)
{
return PartialView("~/Views/Writing/_WritingContextContent.cshtml", model);
}
logger.LogWarning("Writing Context could not resolve scene. ProjectID={ProjectID} BookID={BookID} ChapterID={ChapterID} SceneID={SceneID}", projectId, bookId, chapterId, sceneId);
return PartialView("~/Views/Writing/_WritingContextContent.cshtml", MissingContextDisplay(projectId, bookId, sceneId));
}
[HttpGet("Intent/Scene")]
public async Task<IActionResult> IntentScene(int? projectId, int? bookId, int? chapterId, int? sceneId)
{
if (!sceneId.HasValue && !chapterId.HasValue)
{
logger.LogWarning("Writing Intent partial requested without ChapterID or SceneID. ProjectID={ProjectID} BookID={BookID}", projectId, bookId);
return PartialView("~/Views/Writing/_WritingIntentContent.cshtml", WaitingIntentDisplay(projectId, bookId, null));
}
var model = await BuildIntentDisplayAsync(projectId, bookId, chapterId, sceneId, "Following Word");
if (model is not null && (model.HasScene || model.HasChapter))
{
return PartialView("~/Views/Writing/_WritingIntentContent.cshtml", model);
}
logger.LogWarning("Writing Intent could not resolve chapter/scene. ProjectID={ProjectID} BookID={BookID} ChapterID={ChapterID} SceneID={SceneID}", projectId, bookId, chapterId, sceneId);
return PartialView("~/Views/Writing/_WritingIntentContent.cshtml", MissingIntentDisplay(projectId, bookId, chapterId, sceneId));
}
private async Task<WritingContextDisplayViewModel?> BuildDisplayAsync(int? projectId, int? bookId, int? chapterId, int? sceneId, string followStatus)
{
if (!sceneId.HasValue)
{
return new WritingContextDisplayViewModel { FollowStatus = followStatus };
}
var context = await storyContext.GetForSceneAsync(sceneId.Value, projectId, bookId);
if (context?.Scene is null)
{
return null;
}
var position = context.Position;
var relationships = await relationshipMap.GetAsync(position, null, true);
var floorPlan = context.Scene.FloorPlanID.HasValue
? await floorPlans.GetEditorAsync(context.Scene.FloorPlanID.Value)
: null;
return new WritingContextDisplayViewModel
{
Context = context,
RelationshipMap = relationships,
FloorPlan = floorPlan,
FollowStatus = followStatus,
SceneOptions = BuildSceneOptions(context.Scene)
};
}
private async Task<WritingIntentDisplayViewModel?> BuildIntentDisplayAsync(int? projectId, int? bookId, int? chapterId, int? sceneId, string followStatus)
{
StoryContextViewModel? context = null;
if (sceneId.HasValue)
{
context = await storyContext.GetForSceneAsync(sceneId.Value, projectId, bookId);
if (context?.Scene is null)
{
return null;
}
}
var resolvedChapterId = context?.Position.ChapterId ?? chapterId;
var chapterWorkflow = resolvedChapterId.HasValue
? await writerWorkspace.GetChapterWorkflowAsync(resolvedChapterId.Value)
: null;
if (chapterWorkflow is not null)
{
if (projectId.HasValue && chapterWorkflow.Project.ProjectID != projectId.Value)
{
return null;
}
if (bookId.HasValue && chapterWorkflow.Book.BookID != bookId.Value)
{
return null;
}
}
if (context is null && chapterWorkflow is null)
{
return null;
}
return new WritingIntentDisplayViewModel
{
Context = context,
ChapterWorkflow = chapterWorkflow,
FollowStatus = followStatus,
SceneOptions = context?.Scene is not null
? BuildSceneOptions(context.Scene)
: BuildSceneOptions(chapterWorkflow)
};
}
private static IReadOnlyList<SelectListItem> BuildSceneOptions(SceneEditViewModel scene)
{
var options = scene.SceneOptions
.Select(x => new SelectListItem(x.Text, x.Value, x.Value == scene.SceneID.ToString()))
.ToList();
if (!options.Any(x => x.Value == scene.SceneID.ToString()))
{
options.Insert(0, new SelectListItem(
$"{scene.Book?.BookDisplayTitle ?? "Book"} / Ch {scene.Chapter?.ChapterNumber:g} / Scene {scene.SceneNumber:g}: {scene.SceneTitle}",
scene.SceneID.ToString(),
true));
}
return options
.OrderByDescending(x => x.Selected)
.ThenBy(x => x.Text)
.ToList();
}
private static IReadOnlyList<SelectListItem> BuildSceneOptions(ChapterWorkflowViewModel? chapterWorkflow)
{
return chapterWorkflow?.Scenes
.Select(x => new SelectListItem(
$"Scene {x.SceneNumber:g}: {x.SceneTitle}",
x.SceneID.ToString()))
.OrderBy(x => x.Text)
.ToList()
?? [];
}
private static WritingContextDisplayViewModel WaitingContextDisplay(int? projectId, int? bookId, Guid? documentGuid) => new()
{
FollowStatus = "No current Word scene received",
EmptyTitle = "No current Word scene received",
EmptyMessage = MissingScopeMessage(projectId, bookId, documentGuid, "Context")
};
private static WritingIntentDisplayViewModel WaitingIntentDisplay(int? projectId, int? bookId, Guid? documentGuid) => new()
{
FollowStatus = "No current Word scene received",
EmptyTitle = "No current Word scene received",
EmptyMessage = MissingScopeMessage(projectId, bookId, documentGuid, "Intent")
};
private static WritingContextDisplayViewModel MissingContextDisplay(int? projectId, int? bookId, int? sceneId) => new()
{
FollowStatus = "Context unavailable",
EmptyTitle = "No Story Intelligence context is available",
EmptyMessage = $"PlotDirector could not load context for scene {sceneId?.ToString() ?? "unknown"} in project {projectId?.ToString() ?? "unknown"}, book {bookId?.ToString() ?? "unknown"}."
};
private static WritingIntentDisplayViewModel MissingIntentDisplay(int? projectId, int? bookId, int? chapterId, int? sceneId) => new()
{
FollowStatus = "Intent unavailable",
EmptyTitle = "No writing intent is available",
EmptyMessage = $"PlotDirector could not load writing intent for chapter {chapterId?.ToString() ?? "unknown"}, scene {sceneId?.ToString() ?? "none"} in project {projectId?.ToString() ?? "unknown"}, book {bookId?.ToString() ?? "unknown"}."
};
private static string MissingScopeMessage(int? projectId, int? bookId, Guid? documentGuid, string screen)
{
if (!projectId.HasValue || !bookId.HasValue)
{
return $"{screen} did not receive a project/book from Word Companion. Reopen it from a linked manuscript, or choose a scene manually.";
}
return $"Waiting for Word Companion to report the current scene for project {projectId}, book {bookId}{(documentGuid.HasValue ? " and this Word document" : "")}.";
}
}

View File

@ -1,213 +0,0 @@
using System.Data;
using System.Text.Json;
using Dapper;
using PlotLine.Models;
namespace PlotLine.Data;
public interface ICharacterEnrichmentRepository
{
Task<CharacterEnrichmentRun> QueueAsync(CharacterEnrichmentQueueRequest request);
Task<CharacterEnrichmentRun> QueueCharacterAsync(CharacterEnrichmentCharacterQueueRequest request);
Task<CharacterEnrichmentRun?> RetryAsync(int bookId, int userId);
Task<CharacterEnrichmentRun?> ClaimNextAsync(int leaseMinutes);
Task<CharacterEnrichmentRun?> GetCurrentAsync(int bookId);
Task<CharacterEnrichmentRun?> GetCurrentForCharacterAsync(int characterId, int userId);
Task<CharacterEnrichmentCharacterStatus?> GetCharacterStatusAsync(int characterId, int userId);
Task UpdateProgressAsync(CharacterEnrichmentProgressUpdate update);
Task CompleteAsync(CharacterEnrichmentCompletionRequest request, IReadOnlyList<CharacterEnrichmentCharacterResult> results);
Task FailAsync(int runId, string errorMessage, string? errorDetail, long? durationMs);
Task<CharacterEnrichmentContext> BuildContextAsync(int projectId, int bookId);
Task<CharacterEnrichmentContext> BuildContextAsync(int projectId, int bookId, int? characterId);
}
public sealed class CharacterEnrichmentRepository(ISqlConnectionFactory connectionFactory) : ICharacterEnrichmentRepository
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
public async Task<CharacterEnrichmentRun> QueueAsync(CharacterEnrichmentQueueRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<CharacterEnrichmentRun>(
"dbo.CharacterEnrichmentRun_Queue",
request,
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentRun> QueueCharacterAsync(CharacterEnrichmentCharacterQueueRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<CharacterEnrichmentRun>(
"dbo.CharacterEnrichmentRun_QueueCharacter",
request,
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentRun?> RetryAsync(int bookId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<CharacterEnrichmentRun>(
"dbo.CharacterEnrichmentRun_Retry",
new { BookID = bookId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentRun?> ClaimNextAsync(int leaseMinutes)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<CharacterEnrichmentRun>(
"dbo.CharacterEnrichmentRun_ClaimNext",
new { LeaseMinutes = leaseMinutes },
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentRun?> GetCurrentAsync(int bookId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<CharacterEnrichmentRun>(
"dbo.CharacterEnrichmentRun_GetCurrentByBook",
new { BookID = bookId },
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentRun?> GetCurrentForCharacterAsync(int characterId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<CharacterEnrichmentRun>(
"dbo.CharacterEnrichmentRun_GetCurrentByCharacter",
new { CharacterID = characterId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentCharacterStatus?> GetCharacterStatusAsync(int characterId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<CharacterEnrichmentCharacterStatus>(
"dbo.CharacterEnrichmentCharacterStatus_Get",
new { CharacterID = characterId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
public async Task UpdateProgressAsync(CharacterEnrichmentProgressUpdate update)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CharacterEnrichmentRun_UpdateProgress",
update,
commandType: CommandType.StoredProcedure);
}
public async Task CompleteAsync(CharacterEnrichmentCompletionRequest request, IReadOnlyList<CharacterEnrichmentCharacterResult> results)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CharacterEnrichmentRun_Complete",
new
{
request.CharacterEnrichmentRunID,
request.Model,
request.InputTokens,
request.OutputTokens,
request.TotalTokens,
request.DurationMs,
ResultsJson = JsonSerializer.Serialize(results, JsonOptions)
},
commandType: CommandType.StoredProcedure);
}
public async Task FailAsync(int runId, string errorMessage, string? errorDetail, long? durationMs)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CharacterEnrichmentRun_Fail",
new
{
CharacterEnrichmentRunID = runId,
ErrorMessage = errorMessage,
ErrorDetail = errorDetail,
DurationMs = durationMs
},
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterEnrichmentContext> BuildContextAsync(int projectId, int bookId)
=> await BuildContextAsync(projectId, bookId, null);
public async Task<CharacterEnrichmentContext> BuildContextAsync(int projectId, int bookId, int? characterId)
{
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync(
"dbo.CharacterEnrichment_ContextGet",
new { ProjectID = projectId, BookID = bookId, CharacterID = characterId },
commandType: CommandType.StoredProcedure);
var book = await result.ReadSingleAsync<ContextBookRow>();
var characters = (await result.ReadAsync<CharacterEnrichmentCharacterContext>()).ToList();
var aliases = (await result.ReadAsync<ContextAliasRow>()).ToList();
var scenes = (await result.ReadAsync<ContextSceneRow>()).ToList();
var aliasesByCharacter = aliases
.GroupBy(alias => alias.CharacterID)
.ToDictionary(group => group.Key, group => (IReadOnlyList<string>)group.Select(alias => alias.Alias).ToList());
var scenesByCharacter = scenes
.GroupBy(scene => scene.CharacterID)
.ToDictionary(
group => group.Key,
group => (IReadOnlyList<CharacterEnrichmentSceneContext>)group.Select(scene => new CharacterEnrichmentSceneContext
{
SceneID = scene.SceneID,
ChapterID = scene.ChapterID,
ChapterNumber = scene.ChapterNumber,
ChapterTitle = scene.ChapterTitle,
SceneNumber = scene.SceneNumber,
SceneTitle = scene.SceneTitle,
SourceText = scene.SourceText
}).ToList());
return new CharacterEnrichmentContext
{
ProjectID = projectId,
BookID = bookId,
BookTitle = book.BookTitle,
StoryEra = book.StoryEra,
SeriesStartDate = book.SeriesStartDate,
Characters = characters.Select(character => new CharacterEnrichmentCharacterContext
{
CharacterID = character.CharacterID,
CharacterName = character.CharacterName,
BirthDate = character.BirthDate,
AgeAtSeriesStart = character.AgeAtSeriesStart,
Height = character.Height,
EyeColour = character.EyeColour,
DefaultDescription = character.DefaultDescription,
Aliases = aliasesByCharacter.GetValueOrDefault(character.CharacterID) ?? [],
Scenes = scenesByCharacter.GetValueOrDefault(character.CharacterID) ?? []
}).ToList()
};
}
private sealed class ContextBookRow
{
public string BookTitle { get; init; } = string.Empty;
public string? StoryEra { get; init; }
public DateTime? SeriesStartDate { get; init; }
}
private sealed class ContextAliasRow
{
public int CharacterID { get; init; }
public string Alias { get; init; } = string.Empty;
}
private sealed class ContextSceneRow
{
public int CharacterID { get; init; }
public int SceneID { get; init; }
public int ChapterID { get; init; }
public decimal ChapterNumber { get; init; }
public string ChapterTitle { get; init; } = string.Empty;
public decimal SceneNumber { get; init; }
public string SceneTitle { get; init; } = string.Empty;
public string SourceText { get; init; } = string.Empty;
}
}

View File

@ -1,170 +0,0 @@
using System.Data;
using Dapper;
using PlotLine.Models;
namespace PlotLine.Data;
public interface ICoreImportBasicLocationReviewRepository
{
Task<CoreImportBasicLocationReview?> GetByPreviewAsync(int userId, Guid previewId);
Task ResetByPreviewAsync(int userId, Guid previewId);
Task<CoreImportBasicLocationReview> CreateAsync(int userId, Guid previewId, int projectId, int bookId, int? buildId);
Task<CoreImportBasicLocationCandidate> SaveCandidateAsync(int reviewId, CoreImportBasicLocationCandidate candidate);
Task DeleteEvidenceAsync(int candidateId);
Task SaveEvidenceAsync(int candidateId, CoreImportBasicLocationEvidence evidence);
Task CompletePreparationAsync(int reviewId, int rawObservationCount, int candidateCount);
Task<(IReadOnlyList<CoreImportBasicLocationCandidate> Candidates, IReadOnlyList<CoreImportBasicLocationEvidence> Evidence)> ListCandidatesAsync(int reviewId);
Task SaveDecisionAsync(int reviewId, CoreImportBasicLocationDecision decision);
Task SetResultAsync(int reviewId, string stableKey, int resultLocationId);
Task MarkCompleteAsync(int reviewId);
Task MarkSkippedAsync(int reviewId);
}
public sealed class CoreImportBasicLocationReviewRepository(ISqlConnectionFactory connectionFactory) : ICoreImportBasicLocationReviewRepository
{
public async Task<CoreImportBasicLocationReview?> GetByPreviewAsync(int userId, Guid previewId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<CoreImportBasicLocationReview>(
"dbo.CoreImportBasicLocationReview_GetByPreview",
new { UserID = userId, PreviewID = previewId },
commandType: CommandType.StoredProcedure);
}
public async Task ResetByPreviewAsync(int userId, Guid previewId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CoreImportBasicLocationReview_ResetByPreview",
new { UserID = userId, PreviewID = previewId },
commandType: CommandType.StoredProcedure);
}
public async Task<CoreImportBasicLocationReview> CreateAsync(int userId, Guid previewId, int projectId, int bookId, int? buildId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<CoreImportBasicLocationReview>(
"dbo.CoreImportBasicLocationReview_Create",
new { UserID = userId, PreviewID = previewId, ProjectID = projectId, BookID = bookId, BuildID = buildId },
commandType: CommandType.StoredProcedure);
}
public async Task<CoreImportBasicLocationCandidate> SaveCandidateAsync(int reviewId, CoreImportBasicLocationCandidate candidate)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<CoreImportBasicLocationCandidate>(
"dbo.CoreImportBasicLocationReview_SaveCandidate",
new
{
CoreImportBasicLocationReviewID = reviewId,
candidate.StableKey,
candidate.ProposedName,
candidate.PreferredName,
candidate.KnownNamesJson,
candidate.ParentLocationID,
candidate.ParentCandidateKey,
candidate.ExistingLocationID,
candidate.FirstChapterNumber,
candidate.FirstSceneNumber,
candidate.FirstOccurrenceIndex,
candidate.ObservationCount,
candidate.SortOrder
},
commandType: CommandType.StoredProcedure);
}
public async Task DeleteEvidenceAsync(int candidateId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CoreImportBasicLocationReview_DeleteEvidence",
new { CoreImportBasicLocationCandidateID = candidateId },
commandType: CommandType.StoredProcedure);
}
public async Task SaveEvidenceAsync(int candidateId, CoreImportBasicLocationEvidence evidence)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CoreImportBasicLocationReview_SaveEvidence",
new
{
CoreImportBasicLocationCandidateID = candidateId,
evidence.SceneID,
evidence.ChapterID,
evidence.ChapterNumber,
evidence.SceneNumber,
evidence.OccurrenceIndex,
evidence.EvidenceText,
evidence.SortOrder
},
commandType: CommandType.StoredProcedure);
}
public async Task CompletePreparationAsync(int reviewId, int rawObservationCount, int candidateCount)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CoreImportBasicLocationReview_CompletePreparation",
new { CoreImportBasicLocationReviewID = reviewId, RawObservationCount = rawObservationCount, CandidateCount = candidateCount },
commandType: CommandType.StoredProcedure);
}
public async Task<(IReadOnlyList<CoreImportBasicLocationCandidate> Candidates, IReadOnlyList<CoreImportBasicLocationEvidence> Evidence)> ListCandidatesAsync(int reviewId)
{
using var connection = connectionFactory.CreateConnection();
using var rows = await connection.QueryMultipleAsync(
"dbo.CoreImportBasicLocationReview_ListCandidates",
new { CoreImportBasicLocationReviewID = reviewId },
commandType: CommandType.StoredProcedure);
var candidates = (await rows.ReadAsync<CoreImportBasicLocationCandidate>()).ToList();
var evidence = (await rows.ReadAsync<CoreImportBasicLocationEvidence>()).ToList();
return (candidates, evidence);
}
public async Task SaveDecisionAsync(int reviewId, CoreImportBasicLocationDecision decision)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CoreImportBasicLocationReview_SaveDecision",
new
{
CoreImportBasicLocationReviewID = reviewId,
decision.StableKey,
decision.PreferredName,
decision.Action,
decision.ParentLocationID,
decision.ParentCandidateKey,
decision.SameAsLocationID,
decision.SameAsCandidateKey
},
commandType: CommandType.StoredProcedure);
}
public async Task SetResultAsync(int reviewId, string stableKey, int resultLocationId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CoreImportBasicLocationReview_SetResult",
new { CoreImportBasicLocationReviewID = reviewId, StableKey = stableKey, ResultLocationID = resultLocationId },
commandType: CommandType.StoredProcedure);
}
public async Task MarkCompleteAsync(int reviewId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CoreImportBasicLocationReview_MarkComplete",
new { CoreImportBasicLocationReviewID = reviewId },
commandType: CommandType.StoredProcedure);
}
public async Task MarkSkippedAsync(int reviewId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.CoreImportBasicLocationReview_MarkSkipped",
new { CoreImportBasicLocationReviewID = reviewId },
commandType: CommandType.StoredProcedure);
}
}

View File

@ -107,9 +107,6 @@ public sealed class IllustrationLibraryRepository(ISqlConnectionFactory connecti
image.MimeType, image.MimeType,
Provider = providerResult.Provider, Provider = providerResult.Provider,
Model = providerResult.Model, Model = providerResult.Model,
providerResult.Quality,
providerResult.Size,
providerResult.OutputFormat,
PromptText = prompt, PromptText = prompt,
PromptTemplateVersion = templateVersion, PromptTemplateVersion = templateVersion,
ProviderRequestID = providerResult.ProviderRequestID ProviderRequestID = providerResult.ProviderRequestID

View File

@ -1,81 +0,0 @@
using System.Data;
using Dapper;
using PlotLine.Models;
namespace PlotLine.Data;
public interface IManuscriptTextStatisticsRepository
{
Task SaveAsync(int sceneId, ManuscriptTextStatistics statistics);
Task<ManuscriptTextStatistics?> GetBySceneAsync(int sceneId);
Task<IReadOnlyDictionary<int, ManuscriptTextStatistics>> ListByChapterAsync(int chapterId);
Task<SceneManuscriptSourceRecord?> GetSourceBySceneAsync(int sceneId);
}
public sealed class ManuscriptTextStatisticsRepository(ISqlConnectionFactory connectionFactory) : IManuscriptTextStatisticsRepository
{
public async Task SaveAsync(int sceneId, ManuscriptTextStatistics statistics)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.SceneTextStatistics_Save",
new
{
SceneID = sceneId,
statistics.TotalWordCount,
statistics.DialogueWordCount,
statistics.ProseWordCount,
statistics.TotalParagraphCount,
statistics.DialogueParagraphCount,
statistics.ProseParagraphCount
},
commandType: CommandType.StoredProcedure);
}
public async Task<ManuscriptTextStatistics?> GetBySceneAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<ManuscriptTextStatistics>(
"dbo.SceneTextStatistics_GetByScene",
new { SceneID = sceneId },
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyDictionary<int, ManuscriptTextStatistics>> ListByChapterAsync(int chapterId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneTextStatisticsRow>(
"dbo.SceneTextStatistics_ListByChapter",
new { ChapterID = chapterId },
commandType: CommandType.StoredProcedure);
return rows.ToDictionary(row => row.SceneID, row => new ManuscriptTextStatistics
{
TotalWordCount = row.TotalWordCount,
DialogueWordCount = row.DialogueWordCount,
ProseWordCount = row.ProseWordCount,
TotalParagraphCount = row.TotalParagraphCount,
DialogueParagraphCount = row.DialogueParagraphCount,
ProseParagraphCount = row.ProseParagraphCount
});
}
public async Task<SceneManuscriptSourceRecord?> GetSourceBySceneAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<SceneManuscriptSourceRecord>(
"dbo.SceneManuscriptSource_GetByScene",
new { SceneID = sceneId },
commandType: CommandType.StoredProcedure);
}
private sealed class SceneTextStatisticsRow
{
public int SceneID { get; init; }
public int TotalWordCount { get; init; }
public int DialogueWordCount { get; init; }
public int ProseWordCount { get; init; }
public int TotalParagraphCount { get; init; }
public int DialogueParagraphCount { get; init; }
public int ProseParagraphCount { get; init; }
}
}

View File

@ -1,161 +0,0 @@
using System.Data;
using Dapper;
using Microsoft.Data.SqlClient;
using PlotLine.Models;
namespace PlotLine.Data;
public interface INameIntelligenceRepository
{
Task<NameReferenceImportStatus?> GetLatestImportAsync(string packageId, string version, CancellationToken cancellationToken = default);
Task<int> StartImportAsync(string packageId, string version, string checksum, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default);
Task CompleteImportAsync(int importId, bool succeeded, int localeRows, int givenNameRows, int relationshipRows, int usageRows, string? errorSummary, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default);
Task UpsertLocalesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default);
Task UpsertGivenNamesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default);
Task UpsertRelationshipsAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default);
Task UpsertUsagesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default);
Task<IReadOnlyList<NameLocale>> ListLocalesAsync(CancellationToken cancellationToken = default);
Task<NameRecognitionResult> RecogniseAsync(string normalisedName, string? localeCode, int? birthYear, CancellationToken cancellationToken = default);
Task<IReadOnlyList<NameRelationshipReference>> ListRelationshipsAsync(int givenNameId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<NameSearchResult>> SearchAsync(int userId, NameSearchFilter filter, CancellationToken cancellationToken = default);
Task<IReadOnlyList<NameUsageDecoration>> ListUsedDecorationsAsync(int userId, int projectId, int? bookId, string normalisedName, string usedScope, CancellationToken cancellationToken = default);
}
public sealed class NameIntelligenceRepository(ISqlConnectionFactory connectionFactory) : INameIntelligenceRepository
{
public async Task<NameReferenceImportStatus?> GetLatestImportAsync(string packageId, string version, CancellationToken cancellationToken = default)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<NameReferenceImportStatus>(Command(
"dbo.NameReferenceImport_GetLatest",
new { PackageID = packageId, SemanticVersion = version },
cancellationToken: cancellationToken));
}
public async Task<int> StartImportAsync(string packageId, string version, string checksum, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default)
=> await connection.QuerySingleAsync<int>(Command(
"dbo.NameReferenceImport_Start",
new { PackageID = packageId, SemanticVersion = version, PackageChecksum = checksum },
transaction,
cancellationToken));
public Task CompleteImportAsync(int importId, bool succeeded, int localeRows, int givenNameRows, int relationshipRows, int usageRows, string? errorSummary, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default)
=> connection.ExecuteAsync(Command(
"dbo.NameReferenceImport_Complete",
new
{
NameReferenceImportID = importId,
Succeeded = succeeded,
LocaleRows = localeRows,
GivenNameRows = givenNameRows,
RelationshipRows = relationshipRows,
UsageRows = usageRows,
ErrorSummary = errorSummary
},
transaction,
cancellationToken));
public Task UpsertLocalesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default)
=> ExecuteTableAsync("dbo.NameLocale_BulkUpsert", "Rows", "dbo.NameLocaleImportTable", rows, connection, transaction, cancellationToken);
public Task UpsertGivenNamesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default)
=> ExecuteTableAsync("dbo.GivenName_BulkUpsert", "Rows", "dbo.GivenNameImportTable", rows, connection, transaction, cancellationToken);
public Task UpsertRelationshipsAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default)
=> ExecuteTableAsync("dbo.NameRelationship_BulkUpsert", "Rows", "dbo.NameRelationshipImportTable", rows, connection, transaction, cancellationToken);
public Task UpsertUsagesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default)
=> ExecuteTableAsync("dbo.NameUsage_BulkUpsert", "Rows", "dbo.NameUsageImportTable", rows, connection, transaction, cancellationToken);
public async Task<IReadOnlyList<NameLocale>> ListLocalesAsync(CancellationToken cancellationToken = default)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<NameLocale>(Command("dbo.NameLocale_List", cancellationToken: cancellationToken));
return rows.ToList();
}
public async Task<NameRecognitionResult> RecogniseAsync(string normalisedName, string? localeCode, int? birthYear, CancellationToken cancellationToken = default)
{
using var connection = connectionFactory.CreateConnection();
using var grid = await connection.QueryMultipleAsync(Command(
"dbo.GivenName_Recognise",
new { NormalisedName = normalisedName, LocaleCode = localeCode, BirthYear = birthYear },
cancellationToken: cancellationToken));
var name = await grid.ReadSingleOrDefaultAsync<GivenNameReference>();
if (name is null)
{
return new NameRecognitionResult();
}
var usage = await grid.ReadSingleOrDefaultAsync<NameUsageProfile>();
var related = (await grid.ReadAsync<NameRelationshipReference>()).ToList();
return new NameRecognitionResult
{
Recognised = true,
Name = name,
Usage = usage,
RelatedForms = related,
Message = "Recognised given-name form."
};
}
public async Task<IReadOnlyList<NameRelationshipReference>> ListRelationshipsAsync(int givenNameId, CancellationToken cancellationToken = default)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<NameRelationshipReference>(Command(
"dbo.GivenName_Relationships",
new { GivenNameID = givenNameId },
cancellationToken: cancellationToken));
return rows.ToList();
}
public async Task<IReadOnlyList<NameSearchResult>> SearchAsync(int userId, NameSearchFilter filter, CancellationToken cancellationToken = default)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<NameSearchResult>(Command(
"dbo.GivenName_Search",
new
{
UserID = userId,
filter.ProjectID,
filter.BookID,
Search = NullIfWhiteSpace(filter.Search),
LocaleCode = NullIfWhiteSpace(filter.LocaleCode),
filter.BirthYear,
GenderPreference = NullIfWhiteSpace(filter.GenderPreference),
filter.GenderThreshold,
NameType = NullIfWhiteSpace(filter.NameType),
filter.MinimumUsage,
filter.UsedScope,
filter.UsedOnly,
filter.Page,
filter.PageSize
},
cancellationToken: cancellationToken));
return rows.ToList();
}
public async Task<IReadOnlyList<NameUsageDecoration>> ListUsedDecorationsAsync(int userId, int projectId, int? bookId, string normalisedName, string usedScope, CancellationToken cancellationToken = default)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<NameUsageDecoration>(Command(
"dbo.GivenName_UsedDecorations",
new { UserID = userId, ProjectID = projectId, BookID = bookId, NormalisedName = normalisedName, UsedScope = usedScope },
cancellationToken: cancellationToken));
return rows.ToList();
}
public IDbConnection CreateConnection() => connectionFactory.CreateConnection();
private static async Task ExecuteTableAsync(string procedure, string parameterName, string typeName, DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken)
{
var parameters = new DynamicParameters();
parameters.Add(parameterName, rows.AsTableValuedParameter(typeName));
await connection.ExecuteAsync(Command(procedure, parameters, transaction, cancellationToken));
}
private static CommandDefinition Command(string commandText, object? parameters = null, IDbTransaction? transaction = null, CancellationToken cancellationToken = default)
=> new(commandText, parameters, transaction, commandType: CommandType.StoredProcedure, cancellationToken: cancellationToken);
private static string? NullIfWhiteSpace(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}

View File

@ -8,16 +8,6 @@ namespace PlotLine.Data;
public interface IOnboardingBuildRepository public interface IOnboardingBuildRepository
{ {
Task<OnboardingManuscriptBuildResult?> BuildAsync(int userId, OnboardingManuscriptBuildRequest request); Task<OnboardingManuscriptBuildResult?> BuildAsync(int userId, OnboardingManuscriptBuildRequest request);
Task<OnboardingManuscriptBuildResult?> GetByPreviewAsync(int userId, Guid previewId);
Task MarkMarkerSyncSentAsync(int buildId);
Task MarkMarkerSyncCompletedAsync(int buildId, string markerStatus, string message);
Task MarkMarkerSyncUserSaveRequiredAsync(int buildId, string message);
Task MarkMarkerSyncFailedAsync(int buildId, string message, string? diagnostics = null);
Task SaveStructuralChapterSourceAsync(StructuralChapterSourceSaveRequest request);
Task SaveSceneManuscriptSourceAsync(SceneManuscriptSourceSaveRequest request);
Task SaveSceneStructuralSummaryAsync(int sceneId, string structuralSummary, string? structuralSetting = null);
Task<IReadOnlyList<SceneManuscriptSourceRecord>> ListSceneManuscriptSourcesByChapterAsync(int chapterId);
Task<IReadOnlyList<SceneManuscriptSourceRecord>> ListSceneManuscriptSourcesByBookAsync(int bookId);
} }
public sealed class OnboardingBuildRepository(ISqlConnectionFactory connectionFactory) : IOnboardingBuildRepository public sealed class OnboardingBuildRepository(ISqlConnectionFactory connectionFactory) : IOnboardingBuildRepository
@ -41,22 +31,6 @@ public sealed class OnboardingBuildRepository(ISqlConnectionFactory connectionFa
}, },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
return await ReadBuildResultAsync(result);
}
public async Task<OnboardingManuscriptBuildResult?> GetByPreviewAsync(int userId, Guid previewId)
{
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync(
"dbo.Onboarding_ManuscriptBuild_GetByPreview",
new { UserID = userId, PreviewID = previewId },
commandType: CommandType.StoredProcedure);
return await ReadBuildResultAsync(result);
}
private static async Task<OnboardingManuscriptBuildResult?> ReadBuildResultAsync(SqlMapper.GridReader result)
{
var header = await result.ReadSingleOrDefaultAsync<OnboardingManuscriptBuildResult>(); var header = await result.ReadSingleOrDefaultAsync<OnboardingManuscriptBuildResult>();
if (header is null) if (header is null)
{ {
@ -87,115 +61,6 @@ public sealed class OnboardingBuildRepository(ISqlConnectionFactory connectionFa
CharacterMappings = characters CharacterMappings = characters
}; };
} }
public async Task MarkMarkerSyncSentAsync(int buildId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.Onboarding_ManuscriptBuild_MarkerSyncSent",
new { BuildID = buildId },
commandType: CommandType.StoredProcedure);
}
public async Task MarkMarkerSyncCompletedAsync(int buildId, string markerStatus, string message)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.Onboarding_ManuscriptBuild_MarkerSyncCompleted",
new { BuildID = buildId, MarkerStatus = markerStatus, Message = message },
commandType: CommandType.StoredProcedure);
}
public async Task MarkMarkerSyncUserSaveRequiredAsync(int buildId, string message)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.Onboarding_ManuscriptBuild_MarkerSyncUserSaveRequired",
new { BuildID = buildId, Message = message },
commandType: CommandType.StoredProcedure);
}
public async Task MarkMarkerSyncFailedAsync(int buildId, string message, string? diagnostics = null)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.Onboarding_ManuscriptBuild_MarkerSyncFailed",
new { BuildID = buildId, Message = message, Diagnostics = diagnostics },
commandType: CommandType.StoredProcedure);
}
public async Task SaveStructuralChapterSourceAsync(StructuralChapterSourceSaveRequest request)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.ManuscriptStructuralChapterSource_Save",
new
{
request.ChapterID,
request.ProjectID,
request.BookID,
request.SourceText,
request.SourceWordCount,
request.SourceCharacterCount,
request.ParagraphMapJson,
request.StructuralParsedJson,
request.DefaultPovName,
request.ExplicitNarratorName
},
commandType: CommandType.StoredProcedure);
}
public async Task SaveSceneManuscriptSourceAsync(SceneManuscriptSourceSaveRequest request)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.SceneManuscriptSource_Save",
new
{
request.SceneID,
request.ProjectID,
request.BookID,
request.ChapterID,
request.SourceText,
request.SourceWordCount,
request.SourceCharacterCount,
request.SourceStartParagraph,
request.SourceEndParagraph,
request.WordStartParagraphIndex,
request.WordEndParagraphIndex,
request.SourceKind
},
commandType: CommandType.StoredProcedure);
}
public async Task SaveSceneStructuralSummaryAsync(int sceneId, string structuralSummary, string? structuralSetting = null)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.SceneStructuralSummary_Save",
new { SceneID = sceneId, StructuralSummary = structuralSummary, StructuralSetting = structuralSetting },
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<SceneManuscriptSourceRecord>> ListSceneManuscriptSourcesByBookAsync(int bookId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneManuscriptSourceRecord>(
"dbo.SceneManuscriptSource_ListByBook",
new { BookID = bookId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<SceneManuscriptSourceRecord>> ListSceneManuscriptSourcesByChapterAsync(int chapterId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneManuscriptSourceRecord>(
"dbo.SceneManuscriptSource_ListByChapter",
new { ChapterID = chapterId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
} }
public sealed class OnboardingManuscriptBuildRequest public sealed class OnboardingManuscriptBuildRequest
@ -222,7 +87,6 @@ public sealed class OnboardingBuildSceneRequest
public string TemporarySceneKey { get; init; } = string.Empty; public string TemporarySceneKey { get; init; } = string.Empty;
public string TemporaryChapterKey { get; init; } = string.Empty; public string TemporaryChapterKey { get; init; } = string.Empty;
public string Title { get; init; } = string.Empty; public string Title { get; init; } = string.Empty;
public string? SuggestedTitle { get; init; }
public int SortOrder { get; init; } public int SortOrder { get; init; }
public int WordCount { get; init; } public int WordCount { get; init; }
} }
@ -233,54 +97,3 @@ public sealed class OnboardingBuildCharacterRequest
public string Name { get; init; } = string.Empty; public string Name { get; init; } = string.Empty;
public int? ExistingCharacterID { get; init; } public int? ExistingCharacterID { get; init; }
} }
public sealed class StructuralChapterSourceSaveRequest
{
public int ChapterID { get; init; }
public int ProjectID { get; init; }
public int BookID { get; init; }
public string SourceText { get; init; } = string.Empty;
public int SourceWordCount { get; init; }
public int SourceCharacterCount { get; init; }
public string? ParagraphMapJson { get; init; }
public string? StructuralParsedJson { get; init; }
public string? DefaultPovName { get; init; }
public string? ExplicitNarratorName { get; init; }
}
public sealed class SceneManuscriptSourceSaveRequest
{
public int SceneID { get; init; }
public int ProjectID { get; init; }
public int BookID { get; init; }
public int ChapterID { get; init; }
public string SourceText { get; init; } = string.Empty;
public int SourceWordCount { get; init; }
public int SourceCharacterCount { get; init; }
public int? SourceStartParagraph { get; init; }
public int? SourceEndParagraph { get; init; }
public int? WordStartParagraphIndex { get; init; }
public int? WordEndParagraphIndex { get; init; }
public string SourceKind { get; init; } = "StructuralImport";
}
public sealed class SceneManuscriptSourceRecord
{
public int SceneID { get; init; }
public decimal SceneNumber { get; init; }
public string? SceneTitle { get; init; }
public string? SceneTitleSource { get; init; }
public int ProjectID { get; init; }
public int BookID { get; init; }
public int ChapterID { get; init; }
public string SourceText { get; init; } = string.Empty;
public int SourceWordCount { get; init; }
public int SourceCharacterCount { get; init; }
public int? SourceStartParagraph { get; init; }
public int? SourceEndParagraph { get; init; }
public int? WordStartParagraphIndex { get; init; }
public int? WordEndParagraphIndex { get; init; }
public string SourceKind { get; init; } = string.Empty;
public string? StructuralSummary { get; init; }
public string? StructuralSetting { get; init; }
}

View File

@ -1,32 +0,0 @@
# Data contract
All four datasets are shared/global and contain no user, project, series, book, character or alias ownership.
## GivenNames
`given_name_key` is the immutable import identity (`gn_elizabeth`); database implementations may additionally use an integer surrogate PK. `display_name` preserves spelling. `normalised_name` is the lookup value. `name_type` is one of `Canonical`, `Independent`, `Diminutive`, `Nickname`, or `Variant`. `is_word_ambiguous` asks parsers to require stronger context. `is_active` supports non-destructive correction.
## NameRelationships
`relationship_key` is immutable import identity. `from_given_name_key` and `to_given_name_key` reference GivenNames. `relationship_type` is one of `ShortForm`, `Diminutive`, `Nickname`, `SpellingVariant`, `AlternativeForm`, or `AnglicisedForm`. `strength` describes confidence in the linguistic relationship, not confidence that two manuscript mentions identify one character. `is_bidirectional` controls graph traversal. `curation_source` preserves provenance.
## NameUsages
Each row covers one recognised form, one locale and one inclusive birth-year range. `male_weight` + `female_weight` equals 1.0 within tolerance. These are gender priors. `usage_weight` is relative recognisability within a locale/era and is suitable for ranking, not as an absolute population probability. `confidence` describes evidential confidence. `source_type` separates direct statistical derivation from blended estimates.
Lookup precedence: known birth year → estimated birth year → story year fallback → nearest band. Use exact locale first. Do not silently treat US evidence as another locale’s direct statistic.
## NameLocales
Supported codes: `en-GB`, `en-IE`, `en-US`, `en-CA`, `en-AU`, `en-NZ`. `evidence_basis` and `default_confidence` make limitations visible.
## Recommended constraints
- Unique stable keys in every table.
- Unique Unicode-aware `GivenNames.NormalisedName` under an explicitly chosen collation.
- Unique active `(FromNameId, ToNameId, RelationshipType)`.
- Unique active `(GivenNameId, LocaleId, BirthYearFrom, BirthYearTo)`.
- All weights/confidence in `[0,1]`; gender weights sum to 1 within `0.0002`.
- Ordered, non-overlapping usage bands.
- Reference tables read-only to ordinary users.

View File

@ -1,30 +0,0 @@
# Methodology and limitations
## Inclusion
The seed selects names with sustained or count-based evidence in US or UK birth-registration series from 1900 onward, then retains every required hand-curated canonical, diminutive, nickname and variant from the prototype. The result is intentionally broad enough to include established names of many linguistic origins used in English-speaking societies.
## Usage profiles
- `en-US`: derived from US Social Security Administration annual national name counts.
- `en-GB`: derived from the unified ONS, National Records of Scotland and NISRA series. Historical ranked rows lacking counts receive a decreasing rank-based proxy so the period remains usable without implying exact births.
- `en-IE`: Northern Ireland evidence with conservative UK/US smoothing; labelled as an estimate rather than Republic of Ireland direct statistics.
- `en-CA`: a documented US/UK evidence blend.
- `en-AU` and `en-NZ`: documented UK/US evidence blends.
Within each locale and band, gender weights use observed counts with light smoothing. Usage weights use a logarithmic scale relative to the strongest name in the same evidence pool, preventing a few extremely common names from flattening the remainder. Zero-count bands retain a low usage weight and a broader all-era gender prior with reduced confidence.
## Sources
- SSA Popular Baby Names: https://www.ssa.gov/oact/babynames/limits.html
- SSA qualifications: https://www.ssa.gov/OACT/babynames/background.html
- ONS baby names: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/livebirths/datasets/babynamesinenglandandwalesfrom1996/1996tocurrent
- UK unified processing and upstream source links: https://github.com/rossbowen/babynames
- SSA mirror and processing documentation: https://github.com/hackerb9/ssa-baby-names
- Curated nickname mappings: https://github.com/dxdc/babynames
- Irish CSO reference for future direct enrichment: https://www.cso.ie/en/statistics/birthsdeathsandmarriages/irishbabiesnames/
## Limitations
Registration spelling does not prove etymological canonicity, and official privacy thresholds omit very rare names. Locale estimates are deliberately labelled. This package is a deterministic knowledge layer, not an onomastic authority or demographic classifier. Future releases may replace blended profiles with direct official observations without changing stable name keys or consumer semantics.

View File

@ -1,28 +0,0 @@
# PlotDirector Name Intelligence reference data v1.0.0
Production-candidate global reference package for deterministic name recognition, ranking and relationship evidence in PlotDirector.
## Scale
- 3,067 recognised given-name forms.
- 525 curated short-form, diminutive, nickname, spelling-variant and alternative-form relationships.
- 92,010 usage profiles: every name × six locales × five inclusive birth-era bands.
- Direct registration-derived profiles for the United States and United Kingdom; explicitly labelled blended estimates for Ireland, Canada, Australia and New Zealand.
## Non-negotiable semantics
This library is **positive evidence, not a whitelist**. Missing names remain eligible for character detection through manuscript evidence. Gender values are priors, not character facts. Relationships generate candidates and continuity warnings; they never auto-merge characters. Existing project-specific `CharacterAliases` remain untouched and authoritative for that project.
## Import order
1. `name_locales.csv`
2. `given_names.csv`
3. `name_relationships.csv`
4. `name_usages.csv`
Upsert using the stable string keys, optionally resolving them to application surrogate keys. Never use CSV row order as identity.
## Review and validation
Start with `REVIEW.md` and the three `review_sample_*.csv` files. `validation_report.json` records the full structural validation result. See `DATA_CONTRACT.md`, `METHODOLOGY.md`, and `VALIDATION.md` before implementation.

View File

@ -1,21 +0,0 @@
# Validation rules
Reject an import atomically if any error rule fails:
1. Exact documented headers; UTF-8; unique, non-empty stable keys.
2. Unique normalised names; trimmed display values; controlled enum/boolean values.
3. Every relationship endpoint and every usage foreign key resolves.
4. No self-relationships or duplicate `(from, to, type)` relationships.
5. All numeric weights are finite and in `[0,1]`.
6. Male and female weights total 1 within `0.0002`.
7. Every name/locale has exactly five ordered, contiguous, non-overlapping bands covering 1900–2026.
8. No ownership columns or project-specific aliases enter the reference package.
9. Package version and import outcome are recorded transactionally.
Behavioural invariants:
- Unknown name → no dictionary evidence, never “not a person”.
- Relationship → candidate/explanation/warning, never automatic identity merge.
- Gender prior → ranking hint, always subordinate to manuscript evidence.
- Used-name status → computed from project/book/series character data at query time.

File diff suppressed because it is too large Load Diff

View File

@ -1,71 +0,0 @@
{
"packageId": "plotdirector-name-intelligence",
"version": "1.0.0",
"status": "production-candidate",
"generatedAt": "2026-08-24",
"positiveEvidenceNotWhitelist": true,
"coverage": {
"birthYears": "1900-2026",
"locales": [
"en-GB",
"en-IE",
"en-US",
"en-CA",
"en-AU",
"en-NZ"
],
"selection": "Established by sustained/count-based US or UK registration evidence, plus required curated forms",
"maximumRecognisedForms": 3000
},
"files": {
"name_locales": {
"file": "name_locales.csv",
"rows": 6
},
"given_names": {
"file": "given_names.csv",
"rows": 3067
},
"name_relationships": {
"file": "name_relationships.csv",
"rows": 525
},
"name_usages": {
"file": "name_usages.csv",
"rows": 92010
}
},
"valueSets": {
"nameType": [
"Canonical",
"Independent",
"Diminutive",
"Nickname",
"Variant"
],
"relationshipType": [
"ShortForm",
"Diminutive",
"Nickname",
"SpellingVariant",
"AlternativeForm",
"AnglicisedForm"
],
"sourceType": [
"OfficialStatisticalDerived",
"OfficialSourceBlendedEstimate"
]
},
"weights": {
"minimum": 0,
"maximum": 1,
"genderSumTolerance": 0.0002
},
"importOrder": [
"name_locales.csv",
"given_names.csv",
"name_relationships.csv",
"name_usages.csv"
],
"encoding": "UTF-8 with CRLF CSV"
}

View File

@ -1,7 +0,0 @@
locale_code,display_name,iso_country_code,evidence_basis,default_confidence,sort_order,is_active
en-GB,United Kingdom,GBR,Direct UK registration series,0.88,1,true
en-IE,Ireland,IRL,Northern Ireland evidence plus UK/US smoothing,0.67,2,true
en-US,United States,USA,Direct SSA registration series,0.92,3,true
en-CA,Canada,CAN,US/UK evidence blend,0.64,4,true
en-AU,Australia,AUS,UK/US evidence blend,0.62,5,true
en-NZ,New Zealand,NZL,UK/US evidence blend,0.62,6,true
1 locale_code display_name iso_country_code evidence_basis default_confidence sort_order is_active
2 en-GB United Kingdom GBR Direct UK registration series 0.88 1 true
3 en-IE Ireland IRL Northern Ireland evidence plus UK/US smoothing 0.67 2 true
4 en-US United States USA Direct SSA registration series 0.92 3 true
5 en-CA Canada CAN US/UK evidence blend 0.64 4 true
6 en-AU Australia AUS UK/US evidence blend 0.62 5 true
7 en-NZ New Zealand NZL UK/US evidence blend 0.62 6 true

View File

@ -1,526 +0,0 @@
relationship_key,from_given_name_key,to_given_name_key,relationship_type,strength,is_bidirectional,is_active,curation_source,notes
nr_000001,gn_abbey,gn_abigail,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000002,gn_abbie,gn_abigail,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000003,gn_abby,gn_abigail,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000004,gn_abby,gn_abigail,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000005,gn_abe,gn_abraham,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000006,gn_addie,gn_adelaide,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000007,gn_addie,gn_adeline,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000008,gn_al,gn_alan,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000009,gn_al,gn_albert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000010,gn_alec,gn_alexander,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000011,gn_alex,gn_alexander,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000012,gn_alex,gn_alexander,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000013,gn_alex,gn_alexandra,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000014,gn_alf,gn_alfred,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000015,gn_alfie,gn_alfred,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000016,gn_alfie,gn_alfred,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000017,gn_ali,gn_alison,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000018,gn_allie,gn_alison,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000019,gn_allison,gn_alison,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000020,gn_ally,gn_alexandra,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000021,gn_andy,gn_andrew,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000022,gn_andy,gn_andrew,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000023,gn_angie,gn_angela,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000024,gn_angie,gn_angela,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000025,gn_angie,gn_angelina,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000026,gn_anne,gn_ann,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000027,gn_annie,gn_ann,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000028,gn_annie,gn_ann,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000029,gn_annie,gn_anna,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000030,gn_annie,gn_anna,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000031,gn_ant,gn_anthony,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000032,gn_archie,gn_archibald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000033,gn_arnie,gn_arnold,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000034,gn_art,gn_arthur,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000035,gn_art,gn_arthur,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000036,gn_artie,gn_arthur,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000037,gn_artie,gn_arthur,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000038,gn_babs,gn_barbara,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000039,gn_barb,gn_barbara,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000040,gn_barb,gn_barbara,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000041,gn_barbie,gn_barbara,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000042,gn_barbie,gn_barbara,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000043,gn_barney,gn_bernard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000044,gn_bastian,gn_sebastian,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000045,gn_bea,gn_beatrice,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000046,gn_becca,gn_rebecca,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000047,gn_becca,gn_rebecca,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000048,gn_becky,gn_rebecca,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000049,gn_becky,gn_rebecca,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000050,gn_bella,gn_isabel,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000051,gn_bella,gn_isabella,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000052,gn_belle,gn_isabella,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000053,gn_ben,gn_benjamin,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000054,gn_ben,gn_benjamin,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000055,gn_benny,gn_benjamin,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000056,gn_benny,gn_benjamin,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000057,gn_bernie,gn_bernard,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000058,gn_bernie,gn_bernard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000059,gn_bert,gn_albert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000060,gn_bert,gn_herbert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000061,gn_bert,gn_robert,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000062,gn_bertie,gn_albert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000063,gn_bess,gn_elizabeth,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000064,gn_bessie,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000065,gn_beth,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000066,gn_beth,gn_elizabeth,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000067,gn_betsy,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000068,gn_betty,gn_elizabeth,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000069,gn_bex,gn_rebecca,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000070,gn_bill,gn_william,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000071,gn_billie,gn_wilhelmina,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000072,gn_billy,gn_william,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000073,gn_billy,gn_william,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000074,gn_bob,gn_robert,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000075,gn_bobbie,gn_roberta,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000076,gn_bobby,gn_robert,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000077,gn_bobby,gn_robert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000078,gn_bridie,gn_bridget,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000079,gn_cam,gn_cameron,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000080,gn_carol,gn_caroline,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000081,gn_carrie,gn_carol,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000082,gn_carrie,gn_caroline,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000083,gn_carrie,gn_caroline,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000084,gn_cass,gn_cassandra,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000085,gn_cass,gn_cassandra,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000086,gn_cassie,gn_cassandra,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000087,gn_cat,gn_catherine,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000088,gn_cathy,gn_catherine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000089,gn_cathy,gn_catherine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000090,gn_charlie,gn_charles,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000091,gn_charlie,gn_charles,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000092,gn_charlie,gn_charlotte,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000093,gn_charlie,gn_charlotte,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000094,gn_chas,gn_charles,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000095,gn_chris,gn_christina,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000096,gn_chris,gn_christina,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000097,gn_chris,gn_christine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000098,gn_chris,gn_christine,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000099,gn_chris,gn_christopher,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000100,gn_chris,gn_christopher,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000101,gn_chrissie,gn_christine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000102,gn_chrissy,gn_christina,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000103,gn_chuck,gn_charles,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000104,gn_cindy,gn_cynthia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000105,gn_cindy,gn_lucinda,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000106,gn_clare,gn_claire,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000107,gn_clint,gn_clinton,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000108,gn_connie,gn_constance,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000109,gn_dan,gn_daniel,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000110,gn_dan,gn_daniel,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000111,gn_dani,gn_danielle,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000112,gn_danny,gn_daniel,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000113,gn_danny,gn_daniel,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000114,gn_danny,gn_danielle,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000115,gn_dave,gn_david,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000116,gn_dave,gn_david,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000117,gn_davey,gn_david,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000118,gn_davy,gn_david,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000119,gn_davy,gn_david,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000120,gn_deb,gn_deborah,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000121,gn_deb,gn_deborah,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000122,gn_debbie,gn_deborah,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000123,gn_debbie,gn_deborah,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000124,gn_debby,gn_deborah,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000125,gn_della,gn_adelaide,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000126,gn_dick,gn_richard,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000127,gn_dolly,gn_dorothy,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000128,gn_dom,gn_dominic,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000129,gn_don,gn_donald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000130,gn_donnie,gn_donald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000131,gn_donny,gn_donald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000132,gn_dora,gn_theodora,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000133,gn_dot,gn_dorothy,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000134,gn_dot,gn_dorothy,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000135,gn_dottie,gn_dorothy,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000136,gn_dottie,gn_dorothy,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000137,gn_drew,gn_andrew,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000138,gn_drew,gn_andrew,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000139,gn_ed,gn_edmund,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000140,gn_ed,gn_edward,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000141,gn_ed,gn_edward,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000142,gn_eddie,gn_edmund,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000143,gn_eddie,gn_edward,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000144,gn_eddie,gn_edward,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000145,gn_edie,gn_edith,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000146,gn_eli,gn_elias,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000147,gn_eli,gn_elijah,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000148,gn_eliza,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000149,gn_eliza,gn_elizabeth,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000150,gn_ella,gn_eleanor,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000151,gn_ellie,gn_eleanor,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000152,gn_ellie,gn_eleanor,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000153,gn_ellie,gn_ellen,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000154,gn_elliot,gn_elliott,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000155,gn_elsie,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000156,gn_emmy,gn_emily,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000157,gn_ernie,gn_ernest,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000158,gn_etta,gn_henrietta,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000159,gn_fanny,gn_frances,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000160,gn_flo,gn_florence,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000161,gn_flo,gn_florence,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000162,gn_florrie,gn_florence,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000163,gn_fran,gn_frances,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000164,gn_fran,gn_frances,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000165,gn_frank,gn_francis,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000166,gn_frankie,gn_frances,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000167,gn_frankie,gn_francis,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000168,gn_frankie,gn_francis,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000169,gn_frankie,gn_frank,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000170,gn_fred,gn_alfred,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000171,gn_fred,gn_frederick,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000172,gn_fred,gn_frederick,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000173,gn_freddie,gn_alfred,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000174,gn_freddie,gn_frederick,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000175,gn_freddie,gn_frederick,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000176,gn_freddy,gn_frederick,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000177,gn_freddy,gn_frederick,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000178,gn_gabby,gn_gabrielle,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000179,gn_gabe,gn_gabriel,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000180,gn_gabe,gn_gabriel,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000181,gn_gabi,gn_gabrielle,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000182,gn_gail,gn_abigail,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000183,gn_gail,gn_abigail,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000184,gn_gene,gn_eugene,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000185,gn_geoff,gn_geoffrey,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000186,gn_georgie,gn_george,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000187,gn_georgie,gn_georgia,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000188,gn_gerry,gn_gerald,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000189,gn_gerry,gn_gerald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000190,gn_gerry,gn_geraldine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000191,gn_gigi,gn_virginia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000192,gn_ginger,gn_virginia,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000193,gn_ginny,gn_virginia,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000194,gn_ginny,gn_virginia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000195,gn_greg,gn_gregory,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000196,gn_greg,gn_gregory,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000197,gn_gus,gn_augustus,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000198,gn_gus,gn_gustavo,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000199,gn_gwen,gn_gwendoline,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000200,gn_gwen,gn_gwendolyn,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000201,gn_hal,gn_harold,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000202,gn_hal,gn_henry,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000203,gn_hank,gn_henry,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000204,gn_harry,gn_harold,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000205,gn_harry,gn_henry,AlternativeForm,0.8800,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000206,gn_harry,gn_henry,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000207,gn_hattie,gn_harriet,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000208,gn_hattie,gn_harriet,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000209,gn_isabella,gn_isabel,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000210,gn_isabelle,gn_isabel,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000211,gn_izzy,gn_isabel,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000212,gn_izzy,gn_isabella,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000213,gn_jack,gn_john,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000214,gn_jackie,gn_jacqueline,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000215,gn_jackie,gn_jacqueline,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000216,gn_jake,gn_jacob,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000217,gn_jamie,gn_james,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000218,gn_janey,gn_jane,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000219,gn_jay,gn_james,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000220,gn_jay,gn_jason,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000221,gn_jeff,gn_jeffrey,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000222,gn_jeffrey,gn_geoffrey,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000223,gn_jen,gn_jennifer,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000224,gn_jen,gn_jennifer,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000225,gn_jenny,gn_jennifer,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000226,gn_jenny,gn_jennifer,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000227,gn_jerry,gn_gerald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000228,gn_jerry,gn_gerald,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000229,gn_jerry,gn_jeremy,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000230,gn_jess,gn_jessica,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000231,gn_jess,gn_jessica,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000232,gn_jessie,gn_jessica,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000233,gn_jessie,gn_jessica,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000234,gn_jez,gn_jeremy,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000235,gn_jim,gn_james,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000236,gn_jimmy,gn_james,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000237,gn_jimmy,gn_james,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000238,gn_jo,gn_joanne,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000239,gn_jo,gn_josephine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000240,gn_jo,gn_josephine,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000241,gn_joe,gn_joseph,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000242,gn_joe,gn_joseph,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000243,gn_joey,gn_joseph,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000244,gn_joey,gn_joseph,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000245,gn_johnny,gn_john,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000246,gn_johnny,gn_john,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000247,gn_johnny,gn_jonathan,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000248,gn_jon,gn_jonathan,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000249,gn_jonny,gn_jonathan,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000250,gn_josh,gn_joshua,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000251,gn_josh,gn_joshua,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000252,gn_josie,gn_josephine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000253,gn_josie,gn_josephine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000254,gn_joy,gn_joyce,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000255,gn_judy,gn_judith,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000256,gn_judy,gn_judith,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000257,gn_julie,gn_julia,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000258,gn_julie,gn_julia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000259,gn_kaitlin,gn_caitlin,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000260,gn_kate,gn_catherine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000261,gn_kate,gn_catherine,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000262,gn_kate,gn_katherine,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000263,gn_katelyn,gn_caitlin,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000264,gn_katherine,gn_catherine,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000265,gn_kathryn,gn_catherine,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000266,gn_kathy,gn_catherine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000267,gn_kathy,gn_katherine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000268,gn_katie,gn_catherine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000269,gn_katie,gn_catherine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000270,gn_katie,gn_katherine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000271,gn_kay,gn_catherine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000272,gn_ken,gn_kenneth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000273,gn_ken,gn_kenneth,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000274,gn_kenny,gn_kenneth,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000275,gn_kenny,gn_kenneth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000276,gn_kim,gn_kimberly,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000277,gn_kit,gn_christopher,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000278,gn_kitty,gn_catherine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000279,gn_kris,gn_christina,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000280,gn_larry,gn_laurence,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000281,gn_larry,gn_lawrence,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000282,gn_laurie,gn_laura,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000283,gn_laurie,gn_laura,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000284,gn_laurie,gn_lauren,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000285,gn_laurie,gn_laurence,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000286,gn_lawrence,gn_laurence,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000287,gn_len,gn_leonard,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000288,gn_lena,gn_helena,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000289,gn_lenny,gn_leonard,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000290,gn_lenny,gn_leonard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000291,gn_leo,gn_leonard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000292,gn_leo,gn_leonard,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000293,gn_leo,gn_leonardo,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000294,gn_lettie,gn_charlotte,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000295,gn_lettie,gn_letitia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000296,gn_lewis,gn_louis,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000297,gn_lex,gn_alexander,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000298,gn_lexi,gn_alexandra,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000299,gn_lexi,gn_alexandra,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000300,gn_lexi,gn_alexis,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000301,gn_lexie,gn_alexandra,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000302,gn_liam,gn_william,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000303,gn_libby,gn_elizabeth,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000304,gn_lillie,gn_lillian,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000305,gn_lily,gn_lillian,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000306,gn_lisa,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000307,gn_liv,gn_olivia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000308,gn_liv,gn_olivia,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000309,gn_livvy,gn_olivia,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000310,gn_liz,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000311,gn_liz,gn_elizabeth,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000312,gn_lizzie,gn_elizabeth,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000313,gn_lizzy,gn_elizabeth,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000314,gn_lori,gn_lorraine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000315,gn_lottie,gn_charlotte,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000316,gn_lottie,gn_charlotte,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000317,gn_lou,gn_louis,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000318,gn_lou,gn_louis,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000319,gn_lou,gn_louise,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000320,gn_louie,gn_louis,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000321,gn_lucy,gn_lucille,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000322,gn_lucy,gn_lucinda,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000323,gn_lulu,gn_louise,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000324,gn_lulu,gn_lucille,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000325,gn_maddie,gn_madeline,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000326,gn_mae,gn_mary,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000327,gn_maggie,gn_margaret,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000328,gn_maggie,gn_margaret,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000329,gn_maisie,gn_margaret,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000330,gn_mamie,gn_mary,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000331,gn_mandy,gn_amanda,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000332,gn_mandy,gn_amanda,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000333,gn_margo,gn_margaret,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000334,gn_mari,gn_marianne,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000335,gn_marie,gn_maria,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000336,gn_marty,gn_martin,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000337,gn_mary,gn_maria,AlternativeForm,0.8800,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000338,gn_matt,gn_matthew,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000339,gn_matt,gn_matthew,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000340,gn_mattie,gn_matilda,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000341,gn_matty,gn_matthew,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000342,gn_matty,gn_matthew,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000343,gn_max,gn_maximilian,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000344,gn_max,gn_maxwell,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000345,gn_meg,gn_margaret,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000346,gn_meg,gn_margaret,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000347,gn_mel,gn_melissa,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000348,gn_mick,gn_michael,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000349,gn_mickey,gn_michael,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000350,gn_mike,gn_michael,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000351,gn_mike,gn_michael,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000352,gn_mikey,gn_michael,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000353,gn_millie,gn_mildred,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000354,gn_millie,gn_millicent,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000355,gn_mimi,gn_miriam,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000356,gn_mindy,gn_melinda,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000357,gn_minnie,gn_minerva,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000358,gn_missy,gn_melissa,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000359,gn_mitch,gn_michelle,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000360,gn_mitch,gn_mitchell,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000361,gn_molly,gn_mary,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000362,gn_monty,gn_montgomery,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000363,gn_nat,gn_natalie,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000364,gn_nate,gn_nathan,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000365,gn_nate,gn_nathaniel,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000366,gn_nate,gn_nathaniel,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000367,gn_nathan,gn_nathaniel,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000368,gn_ned,gn_edmund,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000369,gn_ned,gn_edward,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000370,gn_nell,gn_eleanor,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000371,gn_nell,gn_ellen,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000372,gn_nell,gn_helen,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000373,gn_nell,gn_penelope,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000374,gn_nellie,gn_eleanor,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000375,gn_nellie,gn_eleanor,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000376,gn_nellie,gn_helen,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000377,gn_nellie,gn_helen,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000378,gn_nelly,gn_helen,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000379,gn_nic,gn_nicole,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000380,gn_nick,gn_dominic,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000381,gn_nick,gn_nicholas,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000382,gn_nick,gn_nicholas,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000383,gn_nicky,gn_nicholas,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000384,gn_nicky,gn_nicholas,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000385,gn_nicky,gn_nicole,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000386,gn_nico,gn_nicholas,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000387,gn_nikki,gn_nicole,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000388,gn_nora,gn_eleanor,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000389,gn_paddy,gn_patrick,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000390,gn_pat,gn_patricia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000391,gn_pat,gn_patricia,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000392,gn_pat,gn_patrick,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000393,gn_pat,gn_patrick,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000394,gn_patsy,gn_patricia,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000395,gn_peggy,gn_margaret,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000396,gn_penny,gn_penelope,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000397,gn_penny,gn_penelope,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000398,gn_pete,gn_peter,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000399,gn_pete,gn_peter,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000400,gn_phil,gn_philip,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000401,gn_phil,gn_philip,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000402,gn_phillip,gn_philip,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000403,gn_polly,gn_mary,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000404,gn_ray,gn_raymond,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000405,gn_ray,gn_raymond,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000406,gn_reg,gn_reginald,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000407,gn_reggie,gn_reginald,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000408,gn_reggie,gn_reginald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000409,gn_rich,gn_richard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000410,gn_rich,gn_richard,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000411,gn_richie,gn_richard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000412,gn_rick,gn_richard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000413,gn_rick,gn_richard,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000414,gn_ricky,gn_richard,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000415,gn_ricky,gn_richard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000416,gn_rob,gn_robert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000417,gn_rob,gn_robert,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000418,gn_robbie,gn_robert,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000419,gn_robbie,gn_robert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000420,gn_rodger,gn_roger,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000421,gn_ron,gn_ronald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000422,gn_ron,gn_ronald,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000423,gn_ronnie,gn_ronald,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000424,gn_ronnie,gn_ronald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000425,gn_rose,gn_rosemary,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000426,gn_rosie,gn_rose,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000427,gn_rosie,gn_rosemary,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000428,gn_rosie,gn_rosemary,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000429,gn_roxie,gn_roxanne,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000430,gn_roxy,gn_roxanne,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000431,gn_sadie,gn_sarah,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000432,gn_sally,gn_sarah,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000433,gn_sam,gn_samantha,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000434,gn_sam,gn_samantha,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000435,gn_sam,gn_samuel,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000436,gn_sam,gn_samuel,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000437,gn_sammie,gn_samantha,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000438,gn_sammy,gn_samantha,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000439,gn_sammy,gn_samuel,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000440,gn_sammy,gn_samuel,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000441,gn_sandra,gn_cassandra,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000442,gn_sandy,gn_alexander,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000443,gn_sandy,gn_sandra,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000444,gn_sandy,gn_sandra,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000445,gn_sara,gn_sarah,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000446,gn_sasha,gn_alexandra,AlternativeForm,0.8800,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000447,gn_seb,gn_sebastian,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000448,gn_shelly,gn_michelle,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000449,gn_sofia,gn_sophia,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000450,gn_sophie,gn_sophia,AlternativeForm,0.8800,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000451,gn_stella,gn_estella,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000452,gn_steph,gn_stephanie,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000453,gn_steve,gn_stephen,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000454,gn_steve,gn_steven,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000455,gn_steven,gn_stephen,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000456,gn_stevie,gn_stephanie,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000457,gn_stevie,gn_stephen,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000458,gn_stevie,gn_steven,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000459,gn_sue,gn_susan,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000460,gn_sue,gn_susan,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000461,gn_sue,gn_suzanne,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000462,gn_susie,gn_susan,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000463,gn_susie,gn_susan,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000464,gn_susy,gn_susan,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000465,gn_suzie,gn_susan,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000466,gn_suzie,gn_suzanne,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000467,gn_suzy,gn_susan,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000468,gn_suzy,gn_suzanne,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000469,gn_tammy,gn_tamara,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000470,gn_ted,gn_edward,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000471,gn_ted,gn_theodore,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000472,gn_teddy,gn_edward,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000473,gn_teddy,gn_edward,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000474,gn_teddy,gn_theodore,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000475,gn_teddy,gn_theodore,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000476,gn_terri,gn_teresa,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000477,gn_tess,gn_teresa,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000478,gn_tessa,gn_teresa,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000479,gn_tessa,gn_teresa,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000480,gn_theo,gn_theodore,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000481,gn_theo,gn_theodore,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000482,gn_theresa,gn_teresa,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000483,gn_tilly,gn_matilda,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000484,gn_tim,gn_timothy,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000485,gn_tim,gn_timothy,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000486,gn_timmy,gn_timothy,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000487,gn_timmy,gn_timothy,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000488,gn_tina,gn_christina,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000489,gn_tina,gn_christina,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000490,gn_tina,gn_christine,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000491,gn_tom,gn_thomas,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000492,gn_tom,gn_thomas,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000493,gn_tommy,gn_thomas,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000494,gn_tommy,gn_thomas,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000495,gn_toni,gn_antoinette,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000496,gn_toni,gn_antonia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000497,gn_tony,gn_anthony,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000498,gn_tony,gn_anthony,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000499,gn_topher,gn_christopher,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000500,gn_tori,gn_victoria,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000501,gn_tori,gn_victoria,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000502,gn_tricia,gn_patricia,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000503,gn_trixie,gn_beatrice,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000504,gn_trudy,gn_gertrude,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000505,gn_vicki,gn_victoria,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000506,gn_vicky,gn_victoria,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000507,gn_vicky,gn_victoria,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000508,gn_vince,gn_vincent,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000509,gn_vinny,gn_vincent,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000510,gn_wally,gn_walter,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000511,gn_wally,gn_walter,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000512,gn_walt,gn_walter,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000513,gn_walt,gn_walter,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000514,gn_wendy,gn_gwendoline,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000515,gn_wendy,gn_gwendolyn,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000516,gn_will,gn_william,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000517,gn_will,gn_william,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000518,gn_willie,gn_william,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000519,gn_willie,gn_william,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000520,gn_winnie,gn_winifred,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
nr_000521,gn_winnie,gn_winifred,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000522,gn_woody,gn_woodrow,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000523,gn_xander,gn_alexander,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000524,gn_zach,gn_zachary,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
nr_000525,gn_zack,gn_zachary,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
1 relationship_key from_given_name_key to_given_name_key relationship_type strength is_bidirectional is_active curation_source notes
2 nr_000001 gn_abbey gn_abigail SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
3 nr_000002 gn_abbie gn_abigail Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
4 nr_000003 gn_abby gn_abigail Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
5 nr_000004 gn_abby gn_abigail Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
6 nr_000005 gn_abe gn_abraham Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
7 nr_000006 gn_addie gn_adelaide Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
8 nr_000007 gn_addie gn_adeline Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
9 nr_000008 gn_al gn_alan Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
10 nr_000009 gn_al gn_albert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
11 nr_000010 gn_alec gn_alexander Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
12 nr_000011 gn_alex gn_alexander Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
13 nr_000012 gn_alex gn_alexander ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
14 nr_000013 gn_alex gn_alexandra ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
15 nr_000014 gn_alf gn_alfred Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
16 nr_000015 gn_alfie gn_alfred Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
17 nr_000016 gn_alfie gn_alfred Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
18 nr_000017 gn_ali gn_alison ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
19 nr_000018 gn_allie gn_alison Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
20 nr_000019 gn_allison gn_alison SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
21 nr_000020 gn_ally gn_alexandra Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
22 nr_000021 gn_andy gn_andrew Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
23 nr_000022 gn_andy gn_andrew Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
24 nr_000023 gn_angie gn_angela Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
25 nr_000024 gn_angie gn_angela Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
26 nr_000025 gn_angie gn_angelina Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
27 nr_000026 gn_anne gn_ann SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
28 nr_000027 gn_annie gn_ann Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
29 nr_000028 gn_annie gn_ann Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
30 nr_000029 gn_annie gn_anna Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
31 nr_000030 gn_annie gn_anna Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
32 nr_000031 gn_ant gn_anthony ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
33 nr_000032 gn_archie gn_archibald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
34 nr_000033 gn_arnie gn_arnold Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
35 nr_000034 gn_art gn_arthur Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
36 nr_000035 gn_art gn_arthur ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
37 nr_000036 gn_artie gn_arthur Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
38 nr_000037 gn_artie gn_arthur Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
39 nr_000038 gn_babs gn_barbara Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
40 nr_000039 gn_barb gn_barbara Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
41 nr_000040 gn_barb gn_barbara ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
42 nr_000041 gn_barbie gn_barbara Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
43 nr_000042 gn_barbie gn_barbara Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
44 nr_000043 gn_barney gn_bernard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
45 nr_000044 gn_bastian gn_sebastian ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
46 nr_000045 gn_bea gn_beatrice ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
47 nr_000046 gn_becca gn_rebecca Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
48 nr_000047 gn_becca gn_rebecca ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
49 nr_000048 gn_becky gn_rebecca Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
50 nr_000049 gn_becky gn_rebecca Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
51 nr_000050 gn_bella gn_isabel ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
52 nr_000051 gn_bella gn_isabella Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
53 nr_000052 gn_belle gn_isabella Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
54 nr_000053 gn_ben gn_benjamin Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
55 nr_000054 gn_ben gn_benjamin ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
56 nr_000055 gn_benny gn_benjamin Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
57 nr_000056 gn_benny gn_benjamin Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
58 nr_000057 gn_bernie gn_bernard Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
59 nr_000058 gn_bernie gn_bernard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
60 nr_000059 gn_bert gn_albert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
61 nr_000060 gn_bert gn_herbert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
62 nr_000061 gn_bert gn_robert Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
63 nr_000062 gn_bertie gn_albert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
64 nr_000063 gn_bess gn_elizabeth Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
65 nr_000064 gn_bessie gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
66 nr_000065 gn_beth gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
67 nr_000066 gn_beth gn_elizabeth ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
68 nr_000067 gn_betsy gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
69 nr_000068 gn_betty gn_elizabeth Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
70 nr_000069 gn_bex gn_rebecca Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
71 nr_000070 gn_bill gn_william Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
72 nr_000071 gn_billie gn_wilhelmina Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
73 nr_000072 gn_billy gn_william Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
74 nr_000073 gn_billy gn_william Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
75 nr_000074 gn_bob gn_robert Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
76 nr_000075 gn_bobbie gn_roberta Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
77 nr_000076 gn_bobby gn_robert Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
78 nr_000077 gn_bobby gn_robert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
79 nr_000078 gn_bridie gn_bridget Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
80 nr_000079 gn_cam gn_cameron ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
81 nr_000080 gn_carol gn_caroline ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
82 nr_000081 gn_carrie gn_carol Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
83 nr_000082 gn_carrie gn_caroline Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
84 nr_000083 gn_carrie gn_caroline Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
85 nr_000084 gn_cass gn_cassandra Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
86 nr_000085 gn_cass gn_cassandra ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
87 nr_000086 gn_cassie gn_cassandra Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
88 nr_000087 gn_cat gn_catherine ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
89 nr_000088 gn_cathy gn_catherine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
90 nr_000089 gn_cathy gn_catherine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
91 nr_000090 gn_charlie gn_charles Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
92 nr_000091 gn_charlie gn_charles Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
93 nr_000092 gn_charlie gn_charlotte Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
94 nr_000093 gn_charlie gn_charlotte Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
95 nr_000094 gn_chas gn_charles ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
96 nr_000095 gn_chris gn_christina Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
97 nr_000096 gn_chris gn_christina ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
98 nr_000097 gn_chris gn_christine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
99 nr_000098 gn_chris gn_christine ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
100 nr_000099 gn_chris gn_christopher Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
101 nr_000100 gn_chris gn_christopher ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
102 nr_000101 gn_chrissie gn_christine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
103 nr_000102 gn_chrissy gn_christina Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
104 nr_000103 gn_chuck gn_charles Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
105 nr_000104 gn_cindy gn_cynthia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
106 nr_000105 gn_cindy gn_lucinda Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
107 nr_000106 gn_clare gn_claire SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
108 nr_000107 gn_clint gn_clinton Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
109 nr_000108 gn_connie gn_constance Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
110 nr_000109 gn_dan gn_daniel Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
111 nr_000110 gn_dan gn_daniel ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
112 nr_000111 gn_dani gn_danielle ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
113 nr_000112 gn_danny gn_daniel Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
114 nr_000113 gn_danny gn_daniel Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
115 nr_000114 gn_danny gn_danielle Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
116 nr_000115 gn_dave gn_david Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
117 nr_000116 gn_dave gn_david ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
118 nr_000117 gn_davey gn_david Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
119 nr_000118 gn_davy gn_david Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
120 nr_000119 gn_davy gn_david Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
121 nr_000120 gn_deb gn_deborah Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
122 nr_000121 gn_deb gn_deborah ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
123 nr_000122 gn_debbie gn_deborah Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
124 nr_000123 gn_debbie gn_deborah Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
125 nr_000124 gn_debby gn_deborah SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
126 nr_000125 gn_della gn_adelaide Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
127 nr_000126 gn_dick gn_richard Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
128 nr_000127 gn_dolly gn_dorothy Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
129 nr_000128 gn_dom gn_dominic ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
130 nr_000129 gn_don gn_donald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
131 nr_000130 gn_donnie gn_donald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
132 nr_000131 gn_donny gn_donald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
133 nr_000132 gn_dora gn_theodora Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
134 nr_000133 gn_dot gn_dorothy Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
135 nr_000134 gn_dot gn_dorothy ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
136 nr_000135 gn_dottie gn_dorothy Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
137 nr_000136 gn_dottie gn_dorothy Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
138 nr_000137 gn_drew gn_andrew Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
139 nr_000138 gn_drew gn_andrew ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
140 nr_000139 gn_ed gn_edmund ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
141 nr_000140 gn_ed gn_edward Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
142 nr_000141 gn_ed gn_edward ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
143 nr_000142 gn_eddie gn_edmund Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
144 nr_000143 gn_eddie gn_edward Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
145 nr_000144 gn_eddie gn_edward Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
146 nr_000145 gn_edie gn_edith Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
147 nr_000146 gn_eli gn_elias Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
148 nr_000147 gn_eli gn_elijah Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
149 nr_000148 gn_eliza gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
150 nr_000149 gn_eliza gn_elizabeth ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
151 nr_000150 gn_ella gn_eleanor Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
152 nr_000151 gn_ellie gn_eleanor Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
153 nr_000152 gn_ellie gn_eleanor Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
154 nr_000153 gn_ellie gn_ellen Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
155 nr_000154 gn_elliot gn_elliott SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
156 nr_000155 gn_elsie gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
157 nr_000156 gn_emmy gn_emily Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
158 nr_000157 gn_ernie gn_ernest Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
159 nr_000158 gn_etta gn_henrietta Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
160 nr_000159 gn_fanny gn_frances Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
161 nr_000160 gn_flo gn_florence Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
162 nr_000161 gn_flo gn_florence ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
163 nr_000162 gn_florrie gn_florence Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
164 nr_000163 gn_fran gn_frances Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
165 nr_000164 gn_fran gn_frances ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
166 nr_000165 gn_frank gn_francis Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
167 nr_000166 gn_frankie gn_frances Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
168 nr_000167 gn_frankie gn_francis Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
169 nr_000168 gn_frankie gn_francis Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
170 nr_000169 gn_frankie gn_frank Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
171 nr_000170 gn_fred gn_alfred ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
172 nr_000171 gn_fred gn_frederick Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
173 nr_000172 gn_fred gn_frederick ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
174 nr_000173 gn_freddie gn_alfred Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
175 nr_000174 gn_freddie gn_frederick Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
176 nr_000175 gn_freddie gn_frederick Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
177 nr_000176 gn_freddy gn_frederick Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
178 nr_000177 gn_freddy gn_frederick Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
179 nr_000178 gn_gabby gn_gabrielle Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
180 nr_000179 gn_gabe gn_gabriel Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
181 nr_000180 gn_gabe gn_gabriel ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
182 nr_000181 gn_gabi gn_gabrielle SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
183 nr_000182 gn_gail gn_abigail Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
184 nr_000183 gn_gail gn_abigail ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
185 nr_000184 gn_gene gn_eugene Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
186 nr_000185 gn_geoff gn_geoffrey ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
187 nr_000186 gn_georgie gn_george Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
188 nr_000187 gn_georgie gn_georgia Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
189 nr_000188 gn_gerry gn_gerald Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
190 nr_000189 gn_gerry gn_gerald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
191 nr_000190 gn_gerry gn_geraldine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
192 nr_000191 gn_gigi gn_virginia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
193 nr_000192 gn_ginger gn_virginia Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
194 nr_000193 gn_ginny gn_virginia Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
195 nr_000194 gn_ginny gn_virginia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
196 nr_000195 gn_greg gn_gregory Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
197 nr_000196 gn_greg gn_gregory ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
198 nr_000197 gn_gus gn_augustus Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
199 nr_000198 gn_gus gn_gustavo Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
200 nr_000199 gn_gwen gn_gwendoline ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
201 nr_000200 gn_gwen gn_gwendolyn Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
202 nr_000201 gn_hal gn_harold Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
203 nr_000202 gn_hal gn_henry Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
204 nr_000203 gn_hank gn_henry Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
205 nr_000204 gn_harry gn_harold Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
206 nr_000205 gn_harry gn_henry AlternativeForm 0.8800 true true PrototypeCurated Potential relationship only; never auto-merge characters
207 nr_000206 gn_harry gn_henry Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
208 nr_000207 gn_hattie gn_harriet Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
209 nr_000208 gn_hattie gn_harriet Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
210 nr_000209 gn_isabella gn_isabel SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
211 nr_000210 gn_isabelle gn_isabel SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
212 nr_000211 gn_izzy gn_isabel Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
213 nr_000212 gn_izzy gn_isabella Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
214 nr_000213 gn_jack gn_john Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
215 nr_000214 gn_jackie gn_jacqueline Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
216 nr_000215 gn_jackie gn_jacqueline Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
217 nr_000216 gn_jake gn_jacob Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
218 nr_000217 gn_jamie gn_james Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
219 nr_000218 gn_janey gn_jane Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
220 nr_000219 gn_jay gn_james Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
221 nr_000220 gn_jay gn_jason ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
222 nr_000221 gn_jeff gn_jeffrey Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
223 nr_000222 gn_jeffrey gn_geoffrey SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
224 nr_000223 gn_jen gn_jennifer Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
225 nr_000224 gn_jen gn_jennifer ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
226 nr_000225 gn_jenny gn_jennifer Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
227 nr_000226 gn_jenny gn_jennifer Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
228 nr_000227 gn_jerry gn_gerald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
229 nr_000228 gn_jerry gn_gerald SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
230 nr_000229 gn_jerry gn_jeremy Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
231 nr_000230 gn_jess gn_jessica Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
232 nr_000231 gn_jess gn_jessica ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
233 nr_000232 gn_jessie gn_jessica Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
234 nr_000233 gn_jessie gn_jessica Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
235 nr_000234 gn_jez gn_jeremy Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
236 nr_000235 gn_jim gn_james Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
237 nr_000236 gn_jimmy gn_james Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
238 nr_000237 gn_jimmy gn_james Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
239 nr_000238 gn_jo gn_joanne Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
240 nr_000239 gn_jo gn_josephine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
241 nr_000240 gn_jo gn_josephine ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
242 nr_000241 gn_joe gn_joseph Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
243 nr_000242 gn_joe gn_joseph ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
244 nr_000243 gn_joey gn_joseph Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
245 nr_000244 gn_joey gn_joseph Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
246 nr_000245 gn_johnny gn_john Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
247 nr_000246 gn_johnny gn_john Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
248 nr_000247 gn_johnny gn_jonathan Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
249 nr_000248 gn_jon gn_jonathan ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
250 nr_000249 gn_jonny gn_jonathan Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
251 nr_000250 gn_josh gn_joshua Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
252 nr_000251 gn_josh gn_joshua ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
253 nr_000252 gn_josie gn_josephine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
254 nr_000253 gn_josie gn_josephine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
255 nr_000254 gn_joy gn_joyce Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
256 nr_000255 gn_judy gn_judith Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
257 nr_000256 gn_judy gn_judith Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
258 nr_000257 gn_julie gn_julia Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
259 nr_000258 gn_julie gn_julia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
260 nr_000259 gn_kaitlin gn_caitlin SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
261 nr_000260 gn_kate gn_catherine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
262 nr_000261 gn_kate gn_catherine ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
263 nr_000262 gn_kate gn_katherine ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
264 nr_000263 gn_katelyn gn_caitlin SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
265 nr_000264 gn_katherine gn_catherine SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
266 nr_000265 gn_kathryn gn_catherine SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
267 nr_000266 gn_kathy gn_catherine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
268 nr_000267 gn_kathy gn_katherine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
269 nr_000268 gn_katie gn_catherine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
270 nr_000269 gn_katie gn_catherine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
271 nr_000270 gn_katie gn_katherine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
272 nr_000271 gn_kay gn_catherine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
273 nr_000272 gn_ken gn_kenneth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
274 nr_000273 gn_ken gn_kenneth ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
275 nr_000274 gn_kenny gn_kenneth Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
276 nr_000275 gn_kenny gn_kenneth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
277 nr_000276 gn_kim gn_kimberly ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
278 nr_000277 gn_kit gn_christopher Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
279 nr_000278 gn_kitty gn_catherine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
280 nr_000279 gn_kris gn_christina Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
281 nr_000280 gn_larry gn_laurence Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
282 nr_000281 gn_larry gn_lawrence Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
283 nr_000282 gn_laurie gn_laura Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
284 nr_000283 gn_laurie gn_laura Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
285 nr_000284 gn_laurie gn_lauren Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
286 nr_000285 gn_laurie gn_laurence Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
287 nr_000286 gn_lawrence gn_laurence SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
288 nr_000287 gn_len gn_leonard ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
289 nr_000288 gn_lena gn_helena Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
290 nr_000289 gn_lenny gn_leonard Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
291 nr_000290 gn_lenny gn_leonard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
292 nr_000291 gn_leo gn_leonard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
293 nr_000292 gn_leo gn_leonard ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
294 nr_000293 gn_leo gn_leonardo Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
295 nr_000294 gn_lettie gn_charlotte Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
296 nr_000295 gn_lettie gn_letitia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
297 nr_000296 gn_lewis gn_louis SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
298 nr_000297 gn_lex gn_alexander Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
299 nr_000298 gn_lexi gn_alexandra Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
300 nr_000299 gn_lexi gn_alexandra Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
301 nr_000300 gn_lexi gn_alexis Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
302 nr_000301 gn_lexie gn_alexandra Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
303 nr_000302 gn_liam gn_william ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
304 nr_000303 gn_libby gn_elizabeth Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
305 nr_000304 gn_lillie gn_lillian SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
306 nr_000305 gn_lily gn_lillian Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
307 nr_000306 gn_lisa gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
308 nr_000307 gn_liv gn_olivia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
309 nr_000308 gn_liv gn_olivia ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
310 nr_000309 gn_livvy gn_olivia Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
311 nr_000310 gn_liz gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
312 nr_000311 gn_liz gn_elizabeth ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
313 nr_000312 gn_lizzie gn_elizabeth Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
314 nr_000313 gn_lizzy gn_elizabeth SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
315 nr_000314 gn_lori gn_lorraine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
316 nr_000315 gn_lottie gn_charlotte Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
317 nr_000316 gn_lottie gn_charlotte Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
318 nr_000317 gn_lou gn_louis Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
319 nr_000318 gn_lou gn_louis ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
320 nr_000319 gn_lou gn_louise ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
321 nr_000320 gn_louie gn_louis Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
322 nr_000321 gn_lucy gn_lucille Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
323 nr_000322 gn_lucy gn_lucinda ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
324 nr_000323 gn_lulu gn_louise Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
325 nr_000324 gn_lulu gn_lucille Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
326 nr_000325 gn_maddie gn_madeline Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
327 nr_000326 gn_mae gn_mary Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
328 nr_000327 gn_maggie gn_margaret Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
329 nr_000328 gn_maggie gn_margaret Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
330 nr_000329 gn_maisie gn_margaret Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
331 nr_000330 gn_mamie gn_mary Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
332 nr_000331 gn_mandy gn_amanda Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
333 nr_000332 gn_mandy gn_amanda Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
334 nr_000333 gn_margo gn_margaret ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
335 nr_000334 gn_mari gn_marianne ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
336 nr_000335 gn_marie gn_maria SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
337 nr_000336 gn_marty gn_martin Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
338 nr_000337 gn_mary gn_maria AlternativeForm 0.8800 true true PrototypeCurated Potential relationship only; never auto-merge characters
339 nr_000338 gn_matt gn_matthew Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
340 nr_000339 gn_matt gn_matthew ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
341 nr_000340 gn_mattie gn_matilda Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
342 nr_000341 gn_matty gn_matthew Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
343 nr_000342 gn_matty gn_matthew Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
344 nr_000343 gn_max gn_maximilian Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
345 nr_000344 gn_max gn_maxwell Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
346 nr_000345 gn_meg gn_margaret Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
347 nr_000346 gn_meg gn_margaret ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
348 nr_000347 gn_mel gn_melissa ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
349 nr_000348 gn_mick gn_michael Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
350 nr_000349 gn_mickey gn_michael Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
351 nr_000350 gn_mike gn_michael Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
352 nr_000351 gn_mike gn_michael ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
353 nr_000352 gn_mikey gn_michael Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
354 nr_000353 gn_millie gn_mildred Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
355 nr_000354 gn_millie gn_millicent Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
356 nr_000355 gn_mimi gn_miriam Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
357 nr_000356 gn_mindy gn_melinda Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
358 nr_000357 gn_minnie gn_minerva Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
359 nr_000358 gn_missy gn_melissa Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
360 nr_000359 gn_mitch gn_michelle Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
361 nr_000360 gn_mitch gn_mitchell Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
362 nr_000361 gn_molly gn_mary Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
363 nr_000362 gn_monty gn_montgomery Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
364 nr_000363 gn_nat gn_natalie ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
365 nr_000364 gn_nate gn_nathan Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
366 nr_000365 gn_nate gn_nathaniel Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
367 nr_000366 gn_nate gn_nathaniel ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
368 nr_000367 gn_nathan gn_nathaniel ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
369 nr_000368 gn_ned gn_edmund Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
370 nr_000369 gn_ned gn_edward Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
371 nr_000370 gn_nell gn_eleanor Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
372 nr_000371 gn_nell gn_ellen Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
373 nr_000372 gn_nell gn_helen Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
374 nr_000373 gn_nell gn_penelope Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
375 nr_000374 gn_nellie gn_eleanor Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
376 nr_000375 gn_nellie gn_eleanor Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
377 nr_000376 gn_nellie gn_helen Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
378 nr_000377 gn_nellie gn_helen Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
379 nr_000378 gn_nelly gn_helen Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
380 nr_000379 gn_nic gn_nicole ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
381 nr_000380 gn_nick gn_dominic Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
382 nr_000381 gn_nick gn_nicholas Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
383 nr_000382 gn_nick gn_nicholas ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
384 nr_000383 gn_nicky gn_nicholas Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
385 nr_000384 gn_nicky gn_nicholas Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
386 nr_000385 gn_nicky gn_nicole Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
387 nr_000386 gn_nico gn_nicholas Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
388 nr_000387 gn_nikki gn_nicole Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
389 nr_000388 gn_nora gn_eleanor Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
390 nr_000389 gn_paddy gn_patrick Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
391 nr_000390 gn_pat gn_patricia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
392 nr_000391 gn_pat gn_patricia ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
393 nr_000392 gn_pat gn_patrick Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
394 nr_000393 gn_pat gn_patrick ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
395 nr_000394 gn_patsy gn_patricia Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
396 nr_000395 gn_peggy gn_margaret Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
397 nr_000396 gn_penny gn_penelope Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
398 nr_000397 gn_penny gn_penelope Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
399 nr_000398 gn_pete gn_peter Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
400 nr_000399 gn_pete gn_peter ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
401 nr_000400 gn_phil gn_philip Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
402 nr_000401 gn_phil gn_philip ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
403 nr_000402 gn_phillip gn_philip SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
404 nr_000403 gn_polly gn_mary Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
405 nr_000404 gn_ray gn_raymond Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
406 nr_000405 gn_ray gn_raymond ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
407 nr_000406 gn_reg gn_reginald ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
408 nr_000407 gn_reggie gn_reginald Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
409 nr_000408 gn_reggie gn_reginald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
410 nr_000409 gn_rich gn_richard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
411 nr_000410 gn_rich gn_richard ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
412 nr_000411 gn_richie gn_richard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
413 nr_000412 gn_rick gn_richard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
414 nr_000413 gn_rick gn_richard ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
415 nr_000414 gn_ricky gn_richard Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
416 nr_000415 gn_ricky gn_richard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
417 nr_000416 gn_rob gn_robert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
418 nr_000417 gn_rob gn_robert ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
419 nr_000418 gn_robbie gn_robert Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
420 nr_000419 gn_robbie gn_robert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
421 nr_000420 gn_rodger gn_roger SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
422 nr_000421 gn_ron gn_ronald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
423 nr_000422 gn_ron gn_ronald ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
424 nr_000423 gn_ronnie gn_ronald Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
425 nr_000424 gn_ronnie gn_ronald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
426 nr_000425 gn_rose gn_rosemary ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
427 nr_000426 gn_rosie gn_rose Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
428 nr_000427 gn_rosie gn_rosemary Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
429 nr_000428 gn_rosie gn_rosemary Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
430 nr_000429 gn_roxie gn_roxanne Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
431 nr_000430 gn_roxy gn_roxanne Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
432 nr_000431 gn_sadie gn_sarah Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
433 nr_000432 gn_sally gn_sarah Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
434 nr_000433 gn_sam gn_samantha Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
435 nr_000434 gn_sam gn_samantha ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
436 nr_000435 gn_sam gn_samuel Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
437 nr_000436 gn_sam gn_samuel ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
438 nr_000437 gn_sammie gn_samantha Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
439 nr_000438 gn_sammy gn_samantha Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
440 nr_000439 gn_sammy gn_samuel Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
441 nr_000440 gn_sammy gn_samuel Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
442 nr_000441 gn_sandra gn_cassandra ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
443 nr_000442 gn_sandy gn_alexander Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
444 nr_000443 gn_sandy gn_sandra Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
445 nr_000444 gn_sandy gn_sandra Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
446 nr_000445 gn_sara gn_sarah SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
447 nr_000446 gn_sasha gn_alexandra AlternativeForm 0.8800 true true PrototypeCurated Potential relationship only; never auto-merge characters
448 nr_000447 gn_seb gn_sebastian ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
449 nr_000448 gn_shelly gn_michelle Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
450 nr_000449 gn_sofia gn_sophia SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
451 nr_000450 gn_sophie gn_sophia AlternativeForm 0.8800 true true PrototypeCurated Potential relationship only; never auto-merge characters
452 nr_000451 gn_stella gn_estella Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
453 nr_000452 gn_steph gn_stephanie ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
454 nr_000453 gn_steve gn_stephen ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
455 nr_000454 gn_steve gn_steven Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
456 nr_000455 gn_steven gn_stephen SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
457 nr_000456 gn_stevie gn_stephanie Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
458 nr_000457 gn_stevie gn_stephen Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
459 nr_000458 gn_stevie gn_steven Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
460 nr_000459 gn_sue gn_susan Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
461 nr_000460 gn_sue gn_susan ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
462 nr_000461 gn_sue gn_suzanne ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
463 nr_000462 gn_susie gn_susan Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
464 nr_000463 gn_susie gn_susan Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
465 nr_000464 gn_susy gn_susan SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
466 nr_000465 gn_suzie gn_susan SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
467 nr_000466 gn_suzie gn_suzanne Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
468 nr_000467 gn_suzy gn_susan SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
469 nr_000468 gn_suzy gn_suzanne Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
470 nr_000469 gn_tammy gn_tamara Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
471 nr_000470 gn_ted gn_edward Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
472 nr_000471 gn_ted gn_theodore Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
473 nr_000472 gn_teddy gn_edward Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
474 nr_000473 gn_teddy gn_edward Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
475 nr_000474 gn_teddy gn_theodore Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
476 nr_000475 gn_teddy gn_theodore Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
477 nr_000476 gn_terri gn_teresa Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
478 nr_000477 gn_tess gn_teresa Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
479 nr_000478 gn_tessa gn_teresa Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
480 nr_000479 gn_tessa gn_teresa Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
481 nr_000480 gn_theo gn_theodore Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
482 nr_000481 gn_theo gn_theodore ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
483 nr_000482 gn_theresa gn_teresa SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
484 nr_000483 gn_tilly gn_matilda Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
485 nr_000484 gn_tim gn_timothy Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
486 nr_000485 gn_tim gn_timothy ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
487 nr_000486 gn_timmy gn_timothy Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
488 nr_000487 gn_timmy gn_timothy Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
489 nr_000488 gn_tina gn_christina Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
490 nr_000489 gn_tina gn_christina ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
491 nr_000490 gn_tina gn_christine ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
492 nr_000491 gn_tom gn_thomas Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
493 nr_000492 gn_tom gn_thomas ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
494 nr_000493 gn_tommy gn_thomas Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
495 nr_000494 gn_tommy gn_thomas Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
496 nr_000495 gn_toni gn_antoinette Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
497 nr_000496 gn_toni gn_antonia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
498 nr_000497 gn_tony gn_anthony Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
499 nr_000498 gn_tony gn_anthony Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
500 nr_000499 gn_topher gn_christopher ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
501 nr_000500 gn_tori gn_victoria Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
502 nr_000501 gn_tori gn_victoria ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
503 nr_000502 gn_tricia gn_patricia ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
504 nr_000503 gn_trixie gn_beatrice Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
505 nr_000504 gn_trudy gn_gertrude Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
506 nr_000505 gn_vicki gn_victoria SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
507 nr_000506 gn_vicky gn_victoria Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
508 nr_000507 gn_vicky gn_victoria Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
509 nr_000508 gn_vince gn_vincent ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
510 nr_000509 gn_vinny gn_vincent Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
511 nr_000510 gn_wally gn_walter Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
512 nr_000511 gn_wally gn_walter Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
513 nr_000512 gn_walt gn_walter Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
514 nr_000513 gn_walt gn_walter ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
515 nr_000514 gn_wendy gn_gwendoline Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
516 nr_000515 gn_wendy gn_gwendolyn Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
517 nr_000516 gn_will gn_william Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
518 nr_000517 gn_will gn_william ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
519 nr_000518 gn_willie gn_william Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
520 nr_000519 gn_willie gn_william Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
521 nr_000520 gn_winnie gn_winifred Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
522 nr_000521 gn_winnie gn_winifred Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
523 nr_000522 gn_woody gn_woodrow Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
524 nr_000523 gn_xander gn_alexander Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
525 nr_000524 gn_zach gn_zachary Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
526 nr_000525 gn_zack gn_zachary Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
{
"ok": true,
"counts": {
"locales": 6,
"givenNames": 3067,
"relationships": 525,
"usages": 92010
},
"coverage": {
"usageGroups": 18402,
"expectedUsageGroups": 18402
},
"errors": [],
"warnings": []
}

View File

@ -75,8 +75,6 @@ public interface IChapterRepository
Task<IReadOnlyList<Chapter>> ListByBookAsync(int bookId); Task<IReadOnlyList<Chapter>> ListByBookAsync(int bookId);
Task<Chapter?> GetAsync(int chapterId); Task<Chapter?> GetAsync(int chapterId);
Task<int> SaveAsync(Chapter chapter); Task<int> SaveAsync(Chapter chapter);
Task UpdatePovCharacterAsync(int chapterId, int? povCharacterId, string source = "Author", decimal? confidence = null, string? evidence = null, string? suggestedName = null);
Task<int> SetScenesPovAsync(int chapterId, int? povCharacterId, bool overwriteAuthor);
Task ArchiveAsync(int chapterId); Task ArchiveAsync(int chapterId);
} }
@ -86,7 +84,7 @@ public interface ISceneRepository
Task<IReadOnlyList<Scene>> ListByFloorPlanAsync(int floorPlanId); Task<IReadOnlyList<Scene>> ListByFloorPlanAsync(int floorPlanId);
Task<Scene?> GetAsync(int sceneId); Task<Scene?> GetAsync(int sceneId);
Task<int> SaveAsync(Scene scene); Task<int> SaveAsync(Scene scene);
Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId, string source = "Author", decimal? confidence = null, string? evidence = null, string? suggestedName = null, bool overwriteAuthor = false); Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId);
Task SaveFloorPlanLinkAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId); Task SaveFloorPlanLinkAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId);
Task SavePurposesAsync(int sceneId, IEnumerable<int> purposeIds); Task SavePurposesAsync(int sceneId, IEnumerable<int> purposeIds);
Task SaveMetricValuesAsync(int sceneId, IEnumerable<SceneMetricValue> metricValues); Task SaveMetricValuesAsync(int sceneId, IEnumerable<SceneMetricValue> metricValues);
@ -143,7 +141,6 @@ public interface ISceneMetricTypeRepository
Task<bool> SetActiveAsync(int metricTypeId, int projectId, bool isActive); Task<bool> SetActiveAsync(int metricTypeId, int projectId, bool isActive);
Task<bool> MoveAsync(int metricTypeId, int projectId, string direction); Task<bool> MoveAsync(int metricTypeId, int projectId, string direction);
Task AddMissingDefaultsAsync(int projectId); Task AddMissingDefaultsAsync(int projectId);
Task<int> BackfillDefaultValuesForProjectAsync(int projectId);
Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId); Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId);
Task<bool> HasProjectMetricsAsync(int projectId); Task<bool> HasProjectMetricsAsync(int projectId);
} }
@ -168,17 +165,8 @@ public interface IPlotRepository
Task<IReadOnlyList<SceneOption>> ListSceneOptionsAsync(int projectId); Task<IReadOnlyList<SceneOption>> ListSceneOptionsAsync(int projectId);
} }
public interface IStoryIntelligenceNarrativeCandidateRepository
{
Task<int> SaveAsync(StoryIntelligenceNarrativeCandidateSeed candidate);
Task SaveEvidenceAsync(int candidateId, int sceneId, string? contributionType, string? evidence, int sortOrder);
Task<IReadOnlyList<StoryIntelligenceNarrativeCandidate>> ListByBookAsync(int bookId);
Task MarkReviewedAsync(int candidateId, string status, string reviewAction, int userId, int? acceptedPlotLineId, int? acceptedPlotThreadId, int? mergeTargetCandidateId = null);
}
public interface IAssetRepository public interface IAssetRepository
{ {
Task<EntityMergeResult?> MergeAsync(int sourceId, int targetId, int? userId);
Task<AssetLookupData> GetLookupsAsync(int? projectId = null); Task<AssetLookupData> GetLookupsAsync(int? projectId = null);
Task<IReadOnlyList<StoryAsset>> ListAssetsAsync(int projectId); Task<IReadOnlyList<StoryAsset>> ListAssetsAsync(int projectId);
Task<StoryAsset?> GetAssetAsync(int storyAssetId); Task<StoryAsset?> GetAssetAsync(int storyAssetId);
@ -219,8 +207,6 @@ public interface ICharacterRepository
Task<int> AddAliasAsync(int characterId, string alias, int? sortOrder); Task<int> AddAliasAsync(int characterId, string alias, int? sortOrder);
Task UpdateAliasAsync(int characterAliasId, string alias, int? sortOrder); Task UpdateAliasAsync(int characterAliasId, string alias, int? sortOrder);
Task DeleteAliasAsync(int characterAliasId); Task DeleteAliasAsync(int characterAliasId);
Task SetPreferredNameAsync(int characterId, string preferredName);
Task<CharacterMergeResult?> MergeAsync(int sourceCharacterId, int targetCharacterId, int? userId);
Task<IReadOnlyList<CharacterSexValue>> ListSexValuesAsync(int ownerUserId); Task<IReadOnlyList<CharacterSexValue>> ListSexValuesAsync(int ownerUserId);
Task<CharacterSexValue?> GetSexValueAsync(int characterSexValueId, int ownerUserId); Task<CharacterSexValue?> GetSexValueAsync(int characterSexValueId, int ownerUserId);
Task<int> GetOrCreateSexValueAsync(int ownerUserId, string sexName); Task<int> GetOrCreateSexValueAsync(int ownerUserId, string sexName);
@ -250,7 +236,6 @@ public interface ICharacterRepository
Task<IReadOnlyList<CharacterRelationship>> ListInitialRelationshipsByCharacterAsync(int characterId); Task<IReadOnlyList<CharacterRelationship>> ListInitialRelationshipsByCharacterAsync(int characterId);
Task<CharacterRelationship?> GetRelationshipAsync(int characterRelationshipId); Task<CharacterRelationship?> GetRelationshipAsync(int characterRelationshipId);
Task<int> SaveRelationshipAsync(CharacterRelationship relationship); Task<int> SaveRelationshipAsync(CharacterRelationship relationship);
Task<int> AcceptRelationshipDevelopmentAsync(RelationshipDevelopmentAcceptance proposal);
Task ArchiveRelationshipAsync(int characterRelationshipId); Task ArchiveRelationshipAsync(int characterRelationshipId);
Task<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsBySceneAsync(int sceneId); Task<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsBySceneAsync(int sceneId);
Task<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsByCharacterAsync(int characterId); Task<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsByCharacterAsync(int characterId);
@ -262,18 +247,14 @@ public interface ICharacterRepository
public interface ILocationRepository public interface ILocationRepository
{ {
Task ReparentAsync(int projectId, int locationId, int parentLocationId, int? expectedParentLocationId, int? userId);
Task<EntityMergeResult?> MergeAsync(int sourceId, int targetId, int? userId);
Task<LocationLookupData> GetLookupsAsync(); Task<LocationLookupData> GetLookupsAsync();
Task<IReadOnlyList<LocationItem>> ListByProjectAsync(int projectId); Task<IReadOnlyList<LocationItem>> ListByProjectAsync(int projectId);
Task<LocationItem?> GetAsync(int locationId); Task<LocationItem?> GetAsync(int locationId);
Task<int> SaveAsync(LocationItem location); Task<int> SaveAsync(LocationItem location);
Task<IReadOnlyList<LocationAlias>> ListAliasesAsync(int locationId); Task<IReadOnlyList<LocationAlias>> ListAliasesAsync(int locationId);
Task<IReadOnlyList<LocationAlias>> ListAliasesByProjectAsync(int projectId);
Task<int> AddAliasAsync(int locationId, string alias, int? sortOrder); Task<int> AddAliasAsync(int locationId, string alias, int? sortOrder);
Task UpdateAliasAsync(int locationAliasId, string alias, int? sortOrder); Task UpdateAliasAsync(int locationAliasId, string alias, int? sortOrder);
Task DeleteAliasAsync(int locationAliasId); Task DeleteAliasAsync(int locationAliasId);
Task SetPreferredNameAsync(int locationId, string preferredName);
Task ArchiveAsync(int locationId); Task ArchiveAsync(int locationId);
Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByProjectAsync(int projectId); Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByProjectAsync(int projectId);
Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByLocationAsync(int locationId); Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByLocationAsync(int locationId);
@ -490,37 +471,6 @@ public sealed class TimelineSceneOverviewRow
public sealed class LocationRepository(ISqlConnectionFactory connectionFactory) : ILocationRepository public sealed class LocationRepository(ISqlConnectionFactory connectionFactory) : ILocationRepository
{ {
public async Task ReparentAsync(int projectId, int locationId, int parentLocationId, int? expectedParentLocationId, int? userId)
{
using var connection = connectionFactory.CreateConnection();
try
{
await connection.ExecuteAsync("dbo.Location_Reparent",
new { ProjectID = projectId, LocationID = locationId, ParentLocationID = parentLocationId,
ExpectedParentLocationID = expectedParentLocationId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
catch (SqlException ex) when (ex.Number == 51000)
{
throw new InvalidOperationException(ex.Message, ex);
}
}
public async Task<EntityMergeResult?> MergeAsync(int sourceId, int targetId, int? userId)
{
using var connection = connectionFactory.CreateConnection();
try
{
return await connection.QuerySingleOrDefaultAsync<EntityMergeResult>(
"dbo.Location_Merge", new { SourceID = sourceId, TargetID = targetId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
catch (SqlException ex) when (ex.Number == 51000)
{
throw new InvalidOperationException(ex.Message, ex);
}
}
public async Task<LocationLookupData> GetLookupsAsync() public async Task<LocationLookupData> GetLookupsAsync()
{ {
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();
@ -575,16 +525,6 @@ public sealed class LocationRepository(ISqlConnectionFactory connectionFactory)
return rows.ToList(); return rows.ToList();
} }
public async Task<IReadOnlyList<LocationAlias>> ListAliasesByProjectAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<LocationAlias>(
"dbo.LocationAlias_ListByProject",
new { ProjectID = projectId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> AddAliasAsync(int locationId, string alias, int? sortOrder) public async Task<int> AddAliasAsync(int locationId, string alias, int? sortOrder)
{ {
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();
@ -612,15 +552,6 @@ public sealed class LocationRepository(ISqlConnectionFactory connectionFactory)
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
public async Task SetPreferredNameAsync(int locationId, string preferredName)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.Location_SetPreferredName",
new { LocationID = locationId, PreferredName = preferredName },
commandType: CommandType.StoredProcedure);
}
public async Task ArchiveAsync(int locationId) public async Task ArchiveAsync(int locationId)
{ {
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();
@ -2166,31 +2097,10 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) :
public async Task HardDeleteForOwnerAsync(int projectId, int userId) public async Task HardDeleteForOwnerAsync(int projectId, int userId)
{ {
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();
connection.Open(); await connection.ExecuteAsync(
using var transaction = connection.BeginTransaction(); "dbo.ProjectHardDelete_ForOwner",
new { ProjectID = projectId, UserID = userId },
try commandType: CommandType.StoredProcedure);
{
await connection.ExecuteAsync(
"dbo.ProjectHardDelete_PrepareDependencies",
new { ProjectID = projectId, UserID = userId, AllowActiveProject = false },
transaction,
commandType: CommandType.StoredProcedure,
commandTimeout: 120);
await connection.ExecuteAsync(
"dbo.ProjectHardDelete_ForOwner",
new { ProjectID = projectId, UserID = userId },
transaction,
commandType: CommandType.StoredProcedure);
transaction.Commit();
}
catch
{
TryRollback(transaction);
throw;
}
} }
public async Task<IReadOnlyList<string>> ListHardDeleteFilePathsForAccountDeletionAsync(int projectId, int userId) public async Task<IReadOnlyList<string>> ListHardDeleteFilePathsForAccountDeletionAsync(int projectId, int userId)
@ -2206,43 +2116,10 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) :
public async Task HardDeleteForAccountDeletionAsync(int projectId, int userId) public async Task HardDeleteForAccountDeletionAsync(int projectId, int userId)
{ {
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();
connection.Open(); await connection.ExecuteAsync(
using var transaction = connection.BeginTransaction(); "dbo.ProjectHardDelete_ForAccountDeletion",
new { ProjectID = projectId, UserID = userId },
try commandType: CommandType.StoredProcedure);
{
await connection.ExecuteAsync(
"dbo.ProjectHardDelete_PrepareDependencies",
new { ProjectID = projectId, UserID = userId, AllowActiveProject = true },
transaction,
commandType: CommandType.StoredProcedure,
commandTimeout: 120);
await connection.ExecuteAsync(
"dbo.ProjectHardDelete_ForAccountDeletion",
new { ProjectID = projectId, UserID = userId },
transaction,
commandType: CommandType.StoredProcedure);
transaction.Commit();
}
catch
{
TryRollback(transaction);
throw;
}
}
private static void TryRollback(IDbTransaction transaction)
{
try
{
transaction.Rollback();
}
catch
{
// The stored procedure may have already rolled back the ambient transaction.
}
} }
} }
@ -2412,8 +2289,6 @@ public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IB
book.BookNumber, book.BookNumber,
book.Description, book.Description,
book.TargetWordCount, book.TargetWordCount,
book.StoryEra,
book.PrimaryLocaleCode,
book.WritingStartDate, book.WritingStartDate,
book.TargetCompletionDate, book.TargetCompletionDate,
book.PublicationDate, book.PublicationDate,
@ -2522,33 +2397,7 @@ public sealed class ChapterRepository(ISqlConnectionFactory connectionFactory) :
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>( return await connection.QuerySingleAsync<int>(
"dbo.Chapter_Save", "dbo.Chapter_Save",
new { chapter.ChapterID, chapter.BookID, chapter.ChapterNumber, chapter.ChapterTitle, chapter.Summary, chapter.ChapterPurposeID, chapter.RevisionStatusID, chapter.POVCharacterID }, new { chapter.ChapterID, chapter.BookID, chapter.ChapterNumber, chapter.ChapterTitle, chapter.Summary, chapter.ChapterPurposeID, chapter.RevisionStatusID },
commandType: CommandType.StoredProcedure);
}
public async Task UpdatePovCharacterAsync(int chapterId, int? povCharacterId, string source = "Author", decimal? confidence = null, string? evidence = null, string? suggestedName = null)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.Chapter_UpdatePovCharacter",
new
{
ChapterID = chapterId,
POVCharacterID = povCharacterId,
POVSource = source,
POVConfidence = confidence,
POVEvidence = evidence,
POVSuggestedName = suggestedName
},
commandType: CommandType.StoredProcedure);
}
public async Task<int> SetScenesPovAsync(int chapterId, int? povCharacterId, bool overwriteAuthor)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.Chapter_SetScenesPov",
new { ChapterID = chapterId, POVCharacterID = povCharacterId, OverwriteAuthor = overwriteAuthor },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
@ -2621,27 +2470,17 @@ public sealed class SceneRepository(ISqlConnectionFactory connectionFactory) : I
scene.RevisionStatusID, scene.RevisionStatusID,
scene.PrimaryLocationID, scene.PrimaryLocationID,
scene.FloorPlanID, scene.FloorPlanID,
scene.InitialFloorPlanFloorID, scene.InitialFloorPlanFloorID
scene.POVCharacterID
}, },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
public async Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId, string source = "Author", decimal? confidence = null, string? evidence = null, string? suggestedName = null, bool overwriteAuthor = false) public async Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId)
{ {
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync( await connection.ExecuteAsync(
"dbo.Scene_UpdatePovCharacter", "dbo.Scene_UpdatePovCharacter",
new new { SceneID = sceneId, POVCharacterID = povCharacterId },
{
SceneID = sceneId,
POVCharacterID = povCharacterId,
POVSource = source,
POVConfidence = confidence,
POVEvidence = evidence,
POVSuggestedName = suggestedName,
OverwriteAuthor = overwriteAuthor
},
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
@ -3025,15 +2864,6 @@ public sealed class SceneMetricTypeRepository(ISqlConnectionFactory connectionFa
await connection.ExecuteAsync("dbo.SceneMetricType_AddMissingDefaultsForProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); await connection.ExecuteAsync("dbo.SceneMetricType_AddMissingDefaultsForProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
} }
public async Task<int> BackfillDefaultValuesForProjectAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.SceneMetric_BackfillDefaultsForProject",
new { ProjectID = projectId },
commandType: CommandType.StoredProcedure);
}
public async Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId) public async Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId)
{ {
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();
@ -3296,122 +3126,8 @@ public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IP
} }
} }
public sealed class StoryIntelligenceNarrativeCandidateRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligenceNarrativeCandidateRepository
{
public async Task<int> SaveAsync(StoryIntelligenceNarrativeCandidateSeed candidate)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.StoryIntelligenceNarrativeCandidate_Save",
new
{
candidate.ProjectID,
candidate.BookID,
candidate.CandidateType,
candidate.CandidateKey,
candidate.ProposedTitle,
candidate.ProposedDescription,
candidate.Category,
candidate.InferredState,
candidate.ChangeType,
candidate.Significance,
candidate.ExistingPlotLineID,
candidate.ExistingPlotThreadID,
candidate.WholeBookPlotSynthesisRunID,
candidate.AuthorPlotSeedID,
candidate.ExistingPlotLineMatchConfidence,
candidate.ExistingPlotLineMatchReason,
candidate.SeedMatchConfidence,
candidate.SeedMatchReason,
candidate.Source,
candidate.RelatedPlotLineID,
candidate.ParentCandidateKey,
candidate.ParentNarrativeCandidateID,
candidate.Confidence
},
commandType: CommandType.StoredProcedure);
}
public async Task SaveEvidenceAsync(int candidateId, int sceneId, string? contributionType, string? evidence, int sortOrder)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.StoryIntelligenceNarrativeCandidateEvidence_Save",
new
{
NarrativeCandidateID = candidateId,
SceneID = sceneId,
ContributionType = contributionType,
Evidence = evidence,
SortOrder = sortOrder
},
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<StoryIntelligenceNarrativeCandidate>> ListByBookAsync(int bookId)
{
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync(
"dbo.StoryIntelligenceNarrativeCandidate_ListByBook",
new { BookID = bookId },
commandType: CommandType.StoredProcedure);
var candidates = (await result.ReadAsync<StoryIntelligenceNarrativeCandidate>()).ToList();
var evidence = (await result.ReadAsync<StoryIntelligenceNarrativeCandidateEvidence>()).ToList();
var events = result.IsConsumed
? []
: (await result.ReadAsync<StoryIntelligenceNarrativeCandidateEvent>()).ToList();
var evidenceByCandidate = evidence
.GroupBy(item => item.NarrativeCandidateID)
.ToDictionary(group => group.Key, group => (IReadOnlyList<StoryIntelligenceNarrativeCandidateEvidence>)group.ToList());
var eventsByCandidate = events
.GroupBy(item => item.NarrativeCandidateID)
.ToDictionary(group => group.Key, group => (IReadOnlyList<StoryIntelligenceNarrativeCandidateEvent>)group.ToList());
foreach (var candidate in candidates)
{
candidate.Evidence = evidenceByCandidate.TryGetValue(candidate.NarrativeCandidateID, out var items) ? items : [];
candidate.Events = eventsByCandidate.TryGetValue(candidate.NarrativeCandidateID, out var candidateEvents) ? candidateEvents : [];
}
return candidates;
}
public async Task MarkReviewedAsync(int candidateId, string status, string reviewAction, int userId, int? acceptedPlotLineId, int? acceptedPlotThreadId, int? mergeTargetCandidateId = null)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.StoryIntelligenceNarrativeCandidate_MarkReviewed",
new
{
NarrativeCandidateID = candidateId,
Status = status,
ReviewAction = reviewAction,
ReviewedByUserID = userId,
AcceptedPlotLineID = acceptedPlotLineId,
AcceptedPlotThreadID = acceptedPlotThreadId,
MergeTargetNarrativeCandidateID = mergeTargetCandidateId
},
commandType: CommandType.StoredProcedure);
}
}
public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : IAssetRepository public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : IAssetRepository
{ {
public async Task<EntityMergeResult?> MergeAsync(int sourceId, int targetId, int? userId)
{
using var connection = connectionFactory.CreateConnection();
try
{
return await connection.QuerySingleOrDefaultAsync<EntityMergeResult>(
"dbo.StoryAsset_Merge", new { SourceID = sourceId, TargetID = targetId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
catch (SqlException ex) when (ex.Number == 51000)
{
throw new InvalidOperationException(ex.Message, ex);
}
}
public async Task<AssetLookupData> GetLookupsAsync(int? projectId = null) public async Task<AssetLookupData> GetLookupsAsync(int? projectId = null)
{ {
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();
@ -3826,31 +3542,6 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
public async Task SetPreferredNameAsync(int characterId, string preferredName)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.Character_SetPreferredName",
new { CharacterID = characterId, PreferredName = preferredName },
commandType: CommandType.StoredProcedure);
}
public async Task<CharacterMergeResult?> MergeAsync(int sourceCharacterId, int targetCharacterId, int? userId)
{
using var connection = connectionFactory.CreateConnection();
try
{
return await connection.QuerySingleOrDefaultAsync<CharacterMergeResult>(
"dbo.Character_Merge",
new { SourceCharacterID = sourceCharacterId, TargetCharacterID = targetCharacterId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
catch (SqlException ex) when (ex.Number == 51000)
{
throw new InvalidOperationException(ex.Message, ex);
}
}
public async Task<IReadOnlyList<CharacterSexValue>> ListSexValuesAsync(int ownerUserId) public async Task<IReadOnlyList<CharacterSexValue>> ListSexValuesAsync(int ownerUserId)
{ {
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();
@ -3980,12 +3671,7 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
sceneCharacter.OutfitDescription, sceneCharacter.OutfitDescription,
sceneCharacter.PhysicalCondition, sceneCharacter.PhysicalCondition,
sceneCharacter.EmotionalState, sceneCharacter.EmotionalState,
sceneCharacter.KnowledgeNotes, sceneCharacter.KnowledgeNotes
sceneCharacter.MatchSource,
sceneCharacter.MatchedText,
sceneCharacter.OccurrenceCount,
sceneCharacter.FirstOccurrenceIndex,
PreserveExistingWhenNull = string.Equals(sceneCharacter.MatchSource, "CoreImportDeterministic", StringComparison.Ordinal)
}, },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
@ -4127,13 +3813,6 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
public async Task<int> AcceptRelationshipDevelopmentAsync(RelationshipDevelopmentAcceptance proposal)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>("dbo.StoryIntelligenceRelationship_AcceptDevelopment", proposal,
commandType: CommandType.StoredProcedure);
}
public async Task<int> SaveRelationshipAsync(CharacterRelationship relationship) public async Task<int> SaveRelationshipAsync(CharacterRelationship relationship)
{ {
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();

View File

@ -0,0 +1,117 @@
using Dapper;
using PlotLine.Models;
namespace PlotLine.Data;
public interface IStoryIntelligenceIllustrationMatchingRepository
{
Task<IReadOnlyList<StoryIntelligenceCharacterIllustrationAssignment>> ListAssignmentsAsync(int projectId);
Task<StoryIntelligenceCharacterIllustrationAssignment> UpsertAssignmentAsync(StoryIntelligenceCharacterIllustrationAssignmentSave request);
Task<StoryIntelligenceIllustrationDemand> UpsertDemandAsync(StoryIntelligenceIllustrationDemandRequest request);
}
public sealed class StoryIntelligenceIllustrationMatchingRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligenceIllustrationMatchingRepository
{
public async Task<IReadOnlyList<StoryIntelligenceCharacterIllustrationAssignment>> ListAssignmentsAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<StoryIntelligenceCharacterIllustrationAssignment>(
"""
SELECT
StoryIntelligenceCharacterIllustrationAssignmentID,
ProjectID,
CharacterKey,
CharacterName,
IllustrationLibraryItemID,
StableCode,
MatchConfidence,
MatchingScore,
CAST(NULL AS nvarchar(max)) AS EvidenceJson,
CAST(NULL AS nvarchar(max)) AS CandidateDiagnosticsJson,
ReasonChosen,
ReasonReassigned,
IsActive,
CreatedUtc,
UpdatedUtc,
AssignmentStatus,
AssignedUtc,
LastValidatedUtc,
EvidenceVersion,
IsFallback
FROM dbo.StoryIntelligenceCharacterIllustrationAssignments
WHERE ProjectID = @ProjectID
AND IsActive = 1;
""",
new { ProjectID = projectId });
return rows.ToList();
}
public async Task<StoryIntelligenceCharacterIllustrationAssignment> UpsertAssignmentAsync(StoryIntelligenceCharacterIllustrationAssignmentSave request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<StoryIntelligenceCharacterIllustrationAssignment>(
"""
SET XACT_ABORT ON;
MERGE dbo.StoryIntelligenceCharacterIllustrationAssignments WITH (HOLDLOCK) AS target
USING (SELECT @ProjectID AS ProjectID, @CharacterKey AS CharacterKey) AS source
ON target.ProjectID = source.ProjectID
AND target.CharacterKey = source.CharacterKey
AND target.IsActive = 1
WHEN MATCHED THEN
UPDATE SET
CharacterName = @CharacterName,
IllustrationLibraryItemID = @IllustrationLibraryItemID,
StableCode = @StableCode,
MatchConfidence = @MatchConfidence,
MatchingScore = @MatchingScore,
EvidenceJson = @EvidenceJson,
CandidateDiagnosticsJson = @CandidateDiagnosticsJson,
ReasonChosen = @ReasonChosen,
ReasonReassigned = @ReasonReassigned,
AssignmentStatus = CASE WHEN @IllustrationLibraryItemID IS NULL THEN N'Fallback' ELSE N'Assigned' END,
LastValidatedUtc = SYSUTCDATETIME(),
EvidenceVersion = N'21K',
IsFallback = CASE WHEN @IllustrationLibraryItemID IS NULL THEN 1 ELSE 0 END,
UpdatedUtc = SYSUTCDATETIME()
WHEN NOT MATCHED THEN
INSERT (ProjectID, CharacterKey, CharacterName, IllustrationLibraryItemID, StableCode, MatchConfidence, MatchingScore, EvidenceJson, CandidateDiagnosticsJson, ReasonChosen, ReasonReassigned, AssignmentStatus, AssignedUtc, LastValidatedUtc, EvidenceVersion, IsFallback)
VALUES (@ProjectID, @CharacterKey, @CharacterName, @IllustrationLibraryItemID, @StableCode, @MatchConfidence, @MatchingScore, @EvidenceJson, @CandidateDiagnosticsJson, @ReasonChosen, @ReasonReassigned, CASE WHEN @IllustrationLibraryItemID IS NULL THEN N'Fallback' ELSE N'Assigned' END, SYSUTCDATETIME(), SYSUTCDATETIME(), N'21K', CASE WHEN @IllustrationLibraryItemID IS NULL THEN 1 ELSE 0 END);
SELECT *
FROM dbo.StoryIntelligenceCharacterIllustrationAssignments
WHERE ProjectID = @ProjectID
AND CharacterKey = @CharacterKey
AND IsActive = 1;
""",
request);
}
public async Task<StoryIntelligenceIllustrationDemand> UpsertDemandAsync(StoryIntelligenceIllustrationDemandRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<StoryIntelligenceIllustrationDemand>(
"""
SET XACT_ABORT ON;
MERGE dbo.StoryIntelligenceIllustrationDemands WITH (HOLDLOCK) AS target
USING (SELECT @ProjectID AS ProjectID, @DemandKey AS DemandKey) AS source
ON ISNULL(target.ProjectID, -1) = ISNULL(source.ProjectID, -1)
AND target.DemandKey = source.DemandKey
WHEN MATCHED THEN
UPDATE SET
RequestCount = RequestCount + 1,
LastRequestedUtc = SYSUTCDATETIME(),
UpdatedUtc = SYSUTCDATETIME()
WHEN NOT MATCHED THEN
INSERT (ProjectID, DemandKey, AgeBand, Presentation, HairColour, SkinTone, HairLength, Glasses, FacialHair)
VALUES (@ProjectID, @DemandKey, @AgeBand, @Presentation, @HairColour, @SkinTone, @HairLength, @Glasses, @FacialHair);
SELECT *
FROM dbo.StoryIntelligenceIllustrationDemands
WHERE ISNULL(ProjectID, -1) = ISNULL(@ProjectID, -1)
AND DemandKey = @DemandKey;
""",
request);
}
}

View File

@ -9,13 +9,9 @@ public interface IStoryIntelligencePipelineRepository
Task<StoryIntelligenceBookPipelineState?> GetByIdForUserAsync(int importSessionId, int userId); Task<StoryIntelligenceBookPipelineState?> GetByIdForUserAsync(int importSessionId, int userId);
Task<StoryIntelligenceBookPipelineState?> GetByBookAsync(int bookId); Task<StoryIntelligenceBookPipelineState?> GetByBookAsync(int bookId);
Task<StoryIntelligenceBookPipelineState?> GetByBookForUserAsync(int bookId, int userId); Task<StoryIntelligenceBookPipelineState?> GetByBookForUserAsync(int bookId, int userId);
Task<StoryIntelligenceReviewCentreSummary?> GetReviewCentreSummaryAsync(int bookId, int userId);
Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId); Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId);
Task<StoryIntelligenceBookPipelineState> UpsertAsync(StoryIntelligenceBookPipelineSaveRequest request); Task<StoryIntelligenceBookPipelineState> UpsertAsync(StoryIntelligenceBookPipelineSaveRequest request);
Task<IReadOnlyList<StoryIntelligenceCommittedRunSummary>> ListCommittedRunsByBookAsync(int bookId, int userId); Task<IReadOnlyList<StoryIntelligenceCommittedRunSummary>> ListCommittedRunsByBookAsync(int bookId, int userId);
Task<StoryIntelligenceSceneAnalysisRecoveryCandidate?> FindNextReadySceneAnalysisAsync();
Task<StoryIntelligenceSceneAnalysisCompletion?> TryCompleteSceneAnalysisAsync(int bookId, int userId);
Task<StoryIntelligenceImportNotificationTarget?> TryBeginCompletionNotificationAsync(int bookId, int userId);
} }
public sealed class StoryIntelligencePipelineRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligencePipelineRepository public sealed class StoryIntelligencePipelineRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligencePipelineRepository
@ -47,15 +43,6 @@ public sealed class StoryIntelligencePipelineRepository(ISqlConnectionFactory co
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
public async Task<StoryIntelligenceReviewCentreSummary?> GetReviewCentreSummaryAsync(int bookId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceReviewCentreSummary>(
"dbo.StoryIntelligenceReviewCentre_GetSummary",
new { BookID = bookId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId) public async Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId)
{ {
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();
@ -80,13 +67,7 @@ public sealed class StoryIntelligencePipelineRepository(ISqlConnectionFactory co
request.CurrentReviewStage, request.CurrentReviewStage,
request.Status, request.Status,
request.CompletedUtc, request.CompletedUtc,
request.LastRunID, request.LastRunID
request.MarkCharacterReviewComplete,
request.MarkLocationReviewComplete,
request.MarkAssetReviewComplete,
request.MarkRelationshipReviewComplete,
request.MarkKnowledgeReviewComplete,
request.MarkNarrativeReviewComplete
}, },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
@ -100,30 +81,4 @@ public sealed class StoryIntelligencePipelineRepository(ISqlConnectionFactory co
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
return rows.ToList(); return rows.ToList();
} }
public async Task<StoryIntelligenceSceneAnalysisRecoveryCandidate?> FindNextReadySceneAnalysisAsync()
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceSceneAnalysisRecoveryCandidate>(
"dbo.StoryIntelligenceBookPipeline_FindNextReadySceneAnalysis",
commandType: CommandType.StoredProcedure);
}
public async Task<StoryIntelligenceSceneAnalysisCompletion?> TryCompleteSceneAnalysisAsync(int bookId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceSceneAnalysisCompletion>(
"dbo.StoryIntelligenceBookPipeline_TryCompleteSceneAnalysis",
new { BookID = bookId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
public async Task<StoryIntelligenceImportNotificationTarget?> TryBeginCompletionNotificationAsync(int bookId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceImportNotificationTarget>(
"dbo.StoryIntelligenceBookPipeline_TryBeginCompletionNotification",
new { BookID = bookId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
} }

View File

@ -9,7 +9,6 @@ public interface IStoryIntelligenceResultRepository
Task<int> SaveAsync(StoryIntelligenceRunSaveRequest request); Task<int> SaveAsync(StoryIntelligenceRunSaveRequest request);
Task<int> QueueAdminTextAsync(StoryIntelligenceRunQueueRequest request); Task<int> QueueAdminTextAsync(StoryIntelligenceRunQueueRequest request);
Task<StoryIntelligenceQueuedRun?> ClaimNextPendingAsync(); Task<StoryIntelligenceQueuedRun?> ClaimNextPendingAsync();
Task<StoryIntelligenceQueuedRun?> ClaimNextPendingFairAsync(int maxConcurrentRunsPerBook, int maxConcurrentRunsPerUser, int claimLeaseMinutes);
Task UpdateProgressAsync( Task UpdateProgressAsync(
int runId, int runId,
string? status = null, string? status = null,
@ -32,11 +31,9 @@ public interface IStoryIntelligenceResultRepository
Task<int> SaveSceneResultAsync(int runId, int? chapterResultId, StoryIntelligenceSceneResultSaveRequest scene); Task<int> SaveSceneResultAsync(int runId, int? chapterResultId, StoryIntelligenceSceneResultSaveRequest scene);
Task<IReadOnlyList<StoryIntelligenceSavedRunListItem>> ListRunsAsync(); Task<IReadOnlyList<StoryIntelligenceSavedRunListItem>> ListRunsAsync();
Task<IReadOnlyList<StoryIntelligenceSavedRun>> ListRunsByBookForUserAsync(int bookId, int userId); Task<IReadOnlyList<StoryIntelligenceSavedRun>> ListRunsByBookForUserAsync(int bookId, int userId);
Task<IReadOnlyList<StoryIntelligenceActiveBookRunSummary>> ListActiveBookRunSummariesForUserAsync(int userId);
Task<StoryIntelligenceSavedRun?> GetRunAsync(int runId); Task<StoryIntelligenceSavedRun?> GetRunAsync(int runId);
Task<StoryIntelligenceSavedChapterResult?> GetChapterResultAsync(int runId); Task<StoryIntelligenceSavedChapterResult?> GetChapterResultAsync(int runId);
Task<IReadOnlyList<StoryIntelligenceSavedSceneResult>> ListSceneResultsAsync(int runId); Task<IReadOnlyList<StoryIntelligenceSavedSceneResult>> ListSceneResultsAsync(int runId);
Task<IReadOnlyList<Scene>> ListChapterScenesAsync(int chapterId);
Task<StoryIntelligenceImportCommit?> GetImportCommitAsync(int runId); Task<StoryIntelligenceImportCommit?> GetImportCommitAsync(int runId);
Task<StoryIntelligenceChapterImportReadiness> GetChapterImportReadinessAsync(int chapterId); Task<StoryIntelligenceChapterImportReadiness> GetChapterImportReadinessAsync(int chapterId);
Task<StoryIntelligenceImportCommitResult> CommitImportAsync(StoryIntelligenceImportCommitRequest request); Task<StoryIntelligenceImportCommitResult> CommitImportAsync(StoryIntelligenceImportCommitRequest request);
@ -323,23 +320,6 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
public async Task<StoryIntelligenceQueuedRun?> ClaimNextPendingFairAsync(
int maxConcurrentRunsPerBook,
int maxConcurrentRunsPerUser,
int claimLeaseMinutes)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceQueuedRun>(
"dbo.StoryIntelligenceRun_ClaimNextPendingFair",
new
{
MaxConcurrentRunsPerBook = Math.Max(1, maxConcurrentRunsPerBook),
MaxConcurrentRunsPerUser = Math.Max(1, maxConcurrentRunsPerUser),
ClaimLeaseMinutes = Math.Max(15, claimLeaseMinutes)
},
commandType: CommandType.StoredProcedure);
}
public async Task UpdateProgressAsync( public async Task UpdateProgressAsync(
int runId, int runId,
string? status = null, string? status = null,
@ -509,16 +489,6 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
return rows.ToList(); return rows.ToList();
} }
public async Task<IReadOnlyList<StoryIntelligenceActiveBookRunSummary>> ListActiveBookRunSummariesForUserAsync(int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<StoryIntelligenceActiveBookRunSummary>(
"dbo.StoryIntelligenceRun_ListActiveBookSummaryForUser",
new { UserID = userId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<StoryIntelligenceSavedRun?> GetRunAsync(int runId) public async Task<StoryIntelligenceSavedRun?> GetRunAsync(int runId)
{ {
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();
@ -547,20 +517,6 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
return rows.ToList(); return rows.ToList();
} }
public async Task<IReadOnlyList<Scene>> ListChapterScenesAsync(int chapterId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Scene>(
"dbo.Scene_ListByChapter",
new { ChapterID = chapterId },
commandType: CommandType.StoredProcedure);
return rows.Where(scene => !scene.IsArchived)
.OrderBy(scene => scene.SortOrder)
.ThenBy(scene => scene.SceneNumber)
.ThenBy(scene => scene.SceneID)
.ToList();
}
public async Task<StoryIntelligenceImportCommit?> GetImportCommitAsync(int runId) public async Task<StoryIntelligenceImportCommit?> GetImportCommitAsync(int runId)
{ {
using var connection = connectionFactory.CreateConnection(); using var connection = connectionFactory.CreateConnection();
@ -610,30 +566,14 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
new { request.ChapterID }, new { request.ChapterID },
transaction, transaction,
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
var existingScenes = new List<Scene>();
var orderedScenes = request.Scenes.OrderBy(scene => scene.TemporarySceneNumber).ToList();
if (!readiness.CanCommit) if (!readiness.CanCommit)
{ {
existingScenes = (await connection.QueryAsync<Scene>( transaction.Rollback();
"dbo.Scene_ListByChapter", return new StoryIntelligenceImportCommitResult
new { request.ChapterID },
transaction,
commandType: CommandType.StoredProcedure))
.Where(scene => !scene.IsArchived)
.OrderBy(scene => scene.SortOrder)
.ThenBy(scene => scene.SceneNumber)
.ThenBy(scene => scene.SceneID)
.ToList();
if (existingScenes.Count != orderedScenes.Count)
{ {
transaction.Rollback(); Success = false,
return new StoryIntelligenceImportCommitResult Message = readiness.BlockReason ?? "This chapter already contains scenes."
{ };
Success = false,
Message = $"This chapter contains {existingScenes.Count:N0} existing scene(s), but Story Intelligence produced {orderedScenes.Count:N0} scene result(s)."
};
}
} }
if (!string.IsNullOrWhiteSpace(request.ChapterSummary) || request.ChapterPurposeID.HasValue) if (!string.IsNullOrWhiteSpace(request.ChapterSummary) || request.ChapterPurposeID.HasValue)
@ -650,22 +590,17 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
var orderedScenes = request.Scenes.OrderBy(scene => scene.TemporarySceneNumber).ToList();
for (var index = 0; index < orderedScenes.Count; index++) for (var index = 0; index < orderedScenes.Count; index++)
{ {
var item = orderedScenes[index]; var item = orderedScenes[index];
var existingScene = item.SceneID.HasValue
? existingScenes.FirstOrDefault(scene => scene.SceneID == item.SceneID.Value)
: existingScenes.Count > index ? existingScenes[index] : null;
var preserveExistingChronology = HasExistingChronology(existingScene);
var operation = "preparing scene"; var operation = "preparing scene";
try try
{ {
operation = "saving scene"; operation = "saving scene";
var relativeTimeText = TrimOptionalText( var relativeTimeText = TrimOptionalText(
preserveExistingChronology item.ParsedScene.Setting?.DateOrTimeReference ?? item.ParsedScene.Setting?.TimeOfDay,
? existingScene?.RelativeTimeText
: item.RelativeTimeText ?? item.ParsedScene.Setting?.DateOrTimeReference ?? item.ParsedScene.Setting?.TimeOfDay,
200, 200,
$"Scene {item.TemporarySceneNumber:N0} relative time", $"Scene {item.TemporarySceneNumber:N0} relative time",
commitWarnings); commitWarnings);
@ -677,18 +612,18 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
"dbo.Scene_Save", "dbo.Scene_Save",
new new
{ {
SceneID = item.SceneID ?? existingScene?.SceneID, SceneID = (int?)null,
ChapterID = request.ChapterID, ChapterID = request.ChapterID,
SceneNumber = Convert.ToDecimal(item.TemporarySceneNumber), SceneNumber = Convert.ToDecimal(item.TemporarySceneNumber),
SceneTitle = sceneTitle, SceneTitle = sceneTitle,
Summary = item.Summary, Summary = item.Summary,
TimeModeID = preserveExistingChronology ? existingScene!.TimeModeID : item.TimeModeID, TimeModeID = request.TimeModeID,
StartDateTime = preserveExistingChronology ? existingScene!.StartDateTime : item.StartDateTime, StartDateTime = (DateTime?)null,
EndDateTime = preserveExistingChronology ? existingScene!.EndDateTime : item.EndDateTime, EndDateTime = (DateTime?)null,
DurationAmount = preserveExistingChronology ? existingScene!.DurationAmount : item.DurationAmount, DurationAmount = (decimal?)null,
DurationUnitID = preserveExistingChronology ? existingScene!.DurationUnitID : item.DurationUnitID, DurationUnitID = (int?)null,
RelativeTimeText = relativeTimeText, RelativeTimeText = relativeTimeText,
TimeConfidenceID = preserveExistingChronology ? existingScene!.TimeConfidenceID : item.TimeConfidenceID, TimeConfidenceID = request.TimeConfidenceID,
ScenePurposeNotes = item.PurposeNotes, ScenePurposeNotes = item.PurposeNotes,
SceneOutcomeNotes = item.OutcomeNotes, SceneOutcomeNotes = item.OutcomeNotes,
RevisionStatusID = request.RevisionStatusID, RevisionStatusID = request.RevisionStatusID,
@ -802,7 +737,7 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
} }
var notes = new List<string>(); var notes = new List<string>();
notes.Add(existingScenes.Count > 0 ? "Enriched existing structural scenes." : "Committed into empty chapter."); notes.Add("Committed into empty chapter.");
notes.AddRange(request.Warnings.Where(warning => !string.IsNullOrWhiteSpace(warning))); notes.AddRange(request.Warnings.Where(warning => !string.IsNullOrWhiteSpace(warning)));
notes.AddRange(commitWarnings.Where(warning => !string.IsNullOrWhiteSpace(warning))); notes.AddRange(commitWarnings.Where(warning => !string.IsNullOrWhiteSpace(warning)));
@ -829,9 +764,7 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
CommitID = commitId, CommitID = commitId,
ScenesCreated = scenesCreated, ScenesCreated = scenesCreated,
MetricsCreated = metricsCreated, MetricsCreated = metricsCreated,
Message = existingScenes.Count > 0 Message = $"Created {scenesCreated:N0} scene(s)."
? $"Enriched {scenesCreated:N0} existing scene(s)."
: $"Created {scenesCreated:N0} scene(s)."
}; };
} }
catch (Exception ex) catch (Exception ex)
@ -881,14 +814,6 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
} }
} }
private static bool HasExistingChronology(Scene? scene)
=> scene is not null
&& (scene.StartDateTime.HasValue
|| scene.EndDateTime.HasValue
|| scene.DurationAmount.HasValue
|| scene.DurationUnitID.HasValue
|| !string.IsNullOrWhiteSpace(scene.RelativeTimeText));
private static string? TrimOptionalText(string? value, int maxLength, string label, ICollection<string> warnings) private static string? TrimOptionalText(string? value, int maxLength, string label, ICollection<string> warnings)
{ {
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))

View File

@ -1,155 +0,0 @@
using System.Data;
using Dapper;
using PlotLine.Models;
namespace PlotLine.Data;
public interface IStoryIntelligenceReviewCandidateRepository
{
Task RefreshPendingRelationshipsAsync(int bookId, int generationId, string payloadJson);
Task<StoryIntelligenceReviewCandidateGeneration> BeginGenerationAsync(int projectId, int bookId, string module, string source);
Task<StoryIntelligenceReviewCandidateGeneration?> GetCurrentGenerationAsync(int bookId, string module);
Task<StoryIntelligenceReviewCandidateRow> SaveCandidateAsync(StoryIntelligenceReviewCandidateSaveRequest request);
Task SaveEvidenceAsync(int reviewCandidateId, int? sceneId, string evidenceText, decimal? confidence, int sortOrder);
Task<StoryIntelligenceReviewCandidateGeneration?> CompleteGenerationAsync(int generationId);
Task FailGenerationAsync(int generationId, string errorMessage);
Task<IReadOnlyList<StoryIntelligenceReviewCandidateRow>> ListPageAsync(int bookId, string module, int pageNumber, int pageSize);
Task<StoryIntelligenceReviewCandidateGeneration?> MarkReviewedAsync(StoryIntelligenceReviewCandidateDecisionRequest request);
Task ResetBookAsync(int bookId);
Task<IReadOnlyList<StoryIntelligenceReviewCandidateRow>> ListLocationsAsync(int bookId);
Task BackfillLocationSignificanceAsync(int bookId, string assessmentsJson);
}
public sealed class StoryIntelligenceReviewCandidateRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligenceReviewCandidateRepository
{
public async Task RefreshPendingRelationshipsAsync(int bookId, int generationId, string payloadJson)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync("dbo.StoryIntelligenceReviewCandidate_RefreshPendingRelationships",
new { BookID = bookId, GenerationID = generationId, PayloadJson = payloadJson }, commandType: CommandType.StoredProcedure);
}
public async Task<StoryIntelligenceReviewCandidateGeneration> BeginGenerationAsync(int projectId, int bookId, string module, string source)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<StoryIntelligenceReviewCandidateGeneration>(
"dbo.StoryIntelligenceReviewCandidateGeneration_Begin",
new { ProjectID = projectId, BookID = bookId, Module = module, Source = source },
commandType: CommandType.StoredProcedure);
}
public async Task<StoryIntelligenceReviewCandidateGeneration?> GetCurrentGenerationAsync(int bookId, string module)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceReviewCandidateGeneration>(
"dbo.StoryIntelligenceReviewCandidateGeneration_GetCurrent",
new { BookID = bookId, Module = module },
commandType: CommandType.StoredProcedure);
}
public async Task<StoryIntelligenceReviewCandidateRow> SaveCandidateAsync(StoryIntelligenceReviewCandidateSaveRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<StoryIntelligenceReviewCandidateRow>(
"dbo.StoryIntelligenceReviewCandidate_Save",
new
{
request.ReviewCandidateGenerationID,
request.ProjectID,
request.BookID,
request.Module,
request.StableKey,
request.CandidateType,
request.DisplayName,
request.SortOrder,
request.Confidence,
request.ExistingCanonicalID,
request.CandidatePayloadJson
},
commandType: CommandType.StoredProcedure);
}
public async Task SaveEvidenceAsync(int reviewCandidateId, int? sceneId, string evidenceText, decimal? confidence, int sortOrder)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.StoryIntelligenceReviewCandidateEvidence_Save",
new
{
ReviewCandidateID = reviewCandidateId,
SceneID = sceneId,
EvidenceText = evidenceText,
Confidence = confidence,
SortOrder = sortOrder
},
commandType: CommandType.StoredProcedure);
}
public async Task<StoryIntelligenceReviewCandidateGeneration?> CompleteGenerationAsync(int generationId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceReviewCandidateGeneration>(
"dbo.StoryIntelligenceReviewCandidateGeneration_Complete",
new { ReviewCandidateGenerationID = generationId },
commandType: CommandType.StoredProcedure);
}
public async Task FailGenerationAsync(int generationId, string errorMessage)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.StoryIntelligenceReviewCandidateGeneration_Fail",
new { ReviewCandidateGenerationID = generationId, ErrorMessage = errorMessage },
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<StoryIntelligenceReviewCandidateRow>> ListPageAsync(int bookId, string module, int pageNumber, int pageSize)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<StoryIntelligenceReviewCandidateRow>(
"dbo.StoryIntelligenceReviewCandidate_ListPage",
new { BookID = bookId, Module = module, PageNumber = pageNumber, PageSize = pageSize },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<StoryIntelligenceReviewCandidateGeneration?> MarkReviewedAsync(StoryIntelligenceReviewCandidateDecisionRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceReviewCandidateGeneration>(
"dbo.StoryIntelligenceReviewCandidate_MarkReviewed",
new
{
request.BookID,
request.Module,
request.StableKey,
request.DecisionAction,
request.DecisionTargetKey,
request.DecisionCanonicalID
},
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<StoryIntelligenceReviewCandidateRow>> ListLocationsAsync(int bookId)
{
using var connection = connectionFactory.CreateConnection();
return (await connection.QueryAsync<StoryIntelligenceReviewCandidateRow>(
"dbo.StoryIntelligenceLocationReview_List", new { BookID = bookId }, commandType: CommandType.StoredProcedure)).ToList();
}
public async Task BackfillLocationSignificanceAsync(int bookId, string assessmentsJson)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync("dbo.StoryIntelligenceLocationReview_BackfillSignificance",
new { BookID = bookId, AssessmentsJson = assessmentsJson }, commandType: CommandType.StoredProcedure);
}
public async Task ResetBookAsync(int bookId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.StoryIntelligenceReviewCandidate_ResetBook",
new { BookID = bookId },
commandType: CommandType.StoredProcedure);
}
}

View File

@ -23,7 +23,6 @@ public interface IStoryMemoryRepository
Task UpsertAssignmentAsync(StoryMemoryIllustrationAssignmentSave request); Task UpsertAssignmentAsync(StoryMemoryIllustrationAssignmentSave request);
Task InvalidateAssignmentAsync(int assignmentId, string reason); Task InvalidateAssignmentAsync(int assignmentId, string reason);
Task<StoryMemoryIllustrationDemand> UpsertDemandAsync(StoryMemoryIllustrationDemandSave request); Task<StoryMemoryIllustrationDemand> UpsertDemandAsync(StoryMemoryIllustrationDemandSave request);
Task<IReadOnlyList<StoryMemoryIllustrationDemand>> ListDemandsAsync(int importSessionId);
Task<string> GetAppearancePreferenceAsync(int importSessionId); Task<string> GetAppearancePreferenceAsync(int importSessionId);
Task<StoryMemoryImportContext> GetImportContextAsync(int importSessionId); Task<StoryMemoryImportContext> GetImportContextAsync(int importSessionId);
Task SetAppearancePreferenceAsync(int importSessionId, string preference); Task SetAppearancePreferenceAsync(int importSessionId, string preference);
@ -364,18 +363,6 @@ public sealed class StoryMemoryRepository(ISqlConnectionFactory connectionFactor
request); request);
} }
public async Task<IReadOnlyList<StoryMemoryIllustrationDemand>> ListDemandsAsync(int importSessionId)
{
using var connection = connectionFactory.CreateConnection();
return (await connection.QueryAsync<StoryMemoryIllustrationDemand>(
"""
SELECT *
FROM dbo.StoryMemoryIllustrationDemands
WHERE ImportSessionID = @ImportSessionID;
""",
new { ImportSessionID = importSessionId })).ToList();
}
public async Task<string> GetAppearancePreferenceAsync(int importSessionId) public async Task<string> GetAppearancePreferenceAsync(int importSessionId)
=> (await GetImportContextAsync(importSessionId)).AppearancePreference; => (await GetImportContextAsync(importSessionId)).AppearancePreference;

View File

@ -1,39 +0,0 @@
using System.Data;
using Dapper;
using PlotLine.Models;
namespace PlotLine.Data;
public sealed class StoryPointHistory
{
public IReadOnlyList<CharacterKnowledgeItem> Knowledge { get; init; } = [];
public IReadOnlyList<SceneAssetLocation> AssetLocations { get; init; } = [];
public IReadOnlyList<AssetCustodyEvent> Custody { get; init; } = [];
public IReadOnlyList<AssetCustodyCharacter> Custodians { get; init; } = [];
public IReadOnlyList<PlotThread> Threads { get; init; } = [];
public IReadOnlyList<ThreadEvent> ThreadEvents { get; init; } = [];
}
public interface IStoryPointRepository
{
Task<StoryPointHistory> GetHistoryAsync(int projectId, int sceneId);
}
public sealed class StoryPointRepository(ISqlConnectionFactory connections) : IStoryPointRepository
{
public async Task<StoryPointHistory> GetHistoryAsync(int projectId, int sceneId)
{
using var connection = connections.CreateConnection();
using var rows = await connection.QueryMultipleAsync("dbo.StoryPoint_GetHistory",
new { ProjectID = projectId, SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return new StoryPointHistory
{
Knowledge = (await rows.ReadAsync<CharacterKnowledgeItem>()).ToList(),
AssetLocations = (await rows.ReadAsync<SceneAssetLocation>()).ToList(),
Custody = (await rows.ReadAsync<AssetCustodyEvent>()).ToList(),
Custodians = (await rows.ReadAsync<AssetCustodyCharacter>()).ToList(),
Threads = (await rows.ReadAsync<PlotThread>()).ToList(),
ThreadEvents = (await rows.ReadAsync<ThreadEvent>()).ToList()
};
}
}

View File

@ -1,365 +0,0 @@
using System.Data;
using System.Text.Json;
using Dapper;
using PlotLine.Models;
namespace PlotLine.Data;
public interface IWholeBookPlotIntelligenceRepository
{
Task SaveAuthorSeedsAsync(AuthorPlotSeedSaveRequest request);
Task<IReadOnlyList<AuthorPlotSeed>> ListAuthorSeedsAsync(int bookId);
Task<WholeBookPlotSynthesisRun> QueueAsync(WholeBookPlotSynthesisQueueRequest request);
Task<WholeBookPlotSynthesisRun?> RetryAsync(int bookId, int userId);
Task<WholeBookPlotSynthesisRun?> ClaimNextAsync(int leaseMinutes);
Task<WholeBookPlotSynthesisRun?> GetCurrentAsync(int bookId);
Task CompleteAsync(WholeBookPlotSynthesisCompletionRequest request, WholeBookPlotSynthesisResult result);
Task FailAsync(int runId, string errorMessage, string? errorDetail, long? durationMs);
Task<WholeBookPlotSynthesisContext> BuildContextAsync(int projectId, int bookId);
}
public sealed class WholeBookPlotIntelligenceRepository(ISqlConnectionFactory connectionFactory) : IWholeBookPlotIntelligenceRepository
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
public async Task SaveAuthorSeedsAsync(AuthorPlotSeedSaveRequest request)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.AuthorPlotSeed_SaveForBook",
new
{
request.ProjectID,
request.BookID,
SeedsJson = JsonSerializer.Serialize(
request.Descriptions
.Where(description => !string.IsNullOrWhiteSpace(description))
.Select(description => new { description = description.Trim() })
.ToList(),
JsonOptions)
},
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<AuthorPlotSeed>> ListAuthorSeedsAsync(int bookId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<AuthorPlotSeed>(
"dbo.AuthorPlotSeed_ListByBook",
new { BookID = bookId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<WholeBookPlotSynthesisRun> QueueAsync(WholeBookPlotSynthesisQueueRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<WholeBookPlotSynthesisRun>(
"dbo.WholeBookPlotSynthesisRun_Queue",
new
{
request.ProjectID,
request.BookID,
request.UserID,
request.SourceStoryIntelligenceRunID,
request.CharacterResolutionVersion
},
commandType: CommandType.StoredProcedure);
}
public async Task<WholeBookPlotSynthesisRun?> RetryAsync(int bookId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WholeBookPlotSynthesisRun>(
"dbo.WholeBookPlotSynthesisRun_Retry",
new { BookID = bookId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
public async Task<WholeBookPlotSynthesisRun?> ClaimNextAsync(int leaseMinutes)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WholeBookPlotSynthesisRun>(
"dbo.WholeBookPlotSynthesisRun_ClaimNext",
new { LeaseMinutes = leaseMinutes },
commandType: CommandType.StoredProcedure);
}
public async Task<WholeBookPlotSynthesisRun?> GetCurrentAsync(int bookId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WholeBookPlotSynthesisRun>(
"dbo.WholeBookPlotSynthesisRun_GetCurrentByBook",
new { BookID = bookId },
commandType: CommandType.StoredProcedure);
}
public async Task CompleteAsync(WholeBookPlotSynthesisCompletionRequest request, WholeBookPlotSynthesisResult result)
{
using var connection = connectionFactory.CreateConnection();
connection.Open();
using var transaction = connection.BeginTransaction();
try
{
await connection.ExecuteAsync(
"dbo.WholeBookPlotSynthesisRun_Complete",
new
{
request.WholeBookPlotSynthesisRunID,
request.Model,
request.OpenAIResponseID,
request.InputTokens,
request.OutputTokens,
request.CachedInputTokens,
request.TotalTokens,
request.DurationMs,
request.ResultJson
},
transaction,
commandType: CommandType.StoredProcedure);
foreach (var plotLine in result.PlotLines)
{
var candidateId = await connection.QuerySingleAsync<int>(
"dbo.WholeBookPlotSynthesisCandidate_Save",
new
{
request.WholeBookPlotSynthesisRunID,
CandidateType = StoryIntelligenceNarrativeCandidateTypes.PlotLine,
CandidateKey = CandidateKey(StoryIntelligenceNarrativeCandidateTypes.PlotLine, plotLine.Key, plotLine.ProposedTitle),
plotLine.ProposedTitle,
ProposedDescription = plotLine.Description,
Category = "Plot Line",
InferredState = plotLine.BookStatus,
ChangeType = plotLine.BookStatus,
plotLine.Significance,
plotLine.ExistingPlotLineID,
ExistingPlotThreadID = (int?)null,
RelatedPlotLineID = (int?)null,
ParentCandidateKey = (string?)null,
ParentNarrativeCandidateID = (int?)null,
plotLine.AuthorPlotSeedID,
plotLine.ExistingPlotLineMatchConfidence,
plotLine.ExistingPlotLineMatchReason,
plotLine.SeedMatchConfidence,
plotLine.SeedMatchReason,
plotLine.Confidence
},
transaction,
commandType: CommandType.StoredProcedure);
var sortOrder = 0;
foreach (var thread in plotLine.Threads)
{
var threadCandidateId = await connection.QuerySingleAsync<int>(
"dbo.WholeBookPlotSynthesisCandidate_Save",
new
{
request.WholeBookPlotSynthesisRunID,
CandidateType = StoryIntelligenceNarrativeCandidateTypes.Thread,
CandidateKey = CandidateKey(StoryIntelligenceNarrativeCandidateTypes.Thread, thread.Key, thread.ProposedTitle),
thread.ProposedTitle,
ProposedDescription = thread.Description,
Category = "Thread",
InferredState = thread.Status,
ChangeType = thread.Status,
Significance = plotLine.Significance,
ExistingPlotLineID = plotLine.ExistingPlotLineID,
thread.ExistingPlotThreadID,
RelatedPlotLineID = plotLine.ExistingPlotLineID,
ParentCandidateKey = CandidateKey(StoryIntelligenceNarrativeCandidateTypes.PlotLine, plotLine.Key, plotLine.ProposedTitle),
ParentNarrativeCandidateID = candidateId,
plotLine.AuthorPlotSeedID,
ExistingPlotLineMatchConfidence = thread.ExistingThreadMatchConfidence,
ExistingPlotLineMatchReason = thread.MatchReason,
SeedMatchConfidence = plotLine.SeedMatchConfidence,
SeedMatchReason = plotLine.SeedMatchReason,
thread.Confidence
},
transaction,
commandType: CommandType.StoredProcedure);
foreach (var item in thread.Events.Where(item => item.SceneID > 0))
{
await connection.ExecuteAsync(
"dbo.WholeBookPlotSynthesisCandidateEvent_Save",
new
{
NarrativeCandidateID = threadCandidateId,
SynthesisRunID = request.WholeBookPlotSynthesisRunID,
EventKey = string.IsNullOrWhiteSpace(item.Key)
? $"{thread.Key}:{item.SceneID}:{item.EventType}:{sortOrder}"
: item.Key,
item.SceneID,
item.EventType,
EventTitle = item.Title,
EventDescription = item.Description,
item.Evidence,
item.Confidence,
SortOrder = sortOrder++
},
transaction,
commandType: CommandType.StoredProcedure);
await connection.ExecuteAsync(
"dbo.StoryIntelligenceNarrativeCandidateEvidence_Save",
new
{
NarrativeCandidateID = threadCandidateId,
item.SceneID,
ContributionType = item.EventType,
Evidence = item.Evidence,
SortOrder = sortOrder
},
transaction,
commandType: CommandType.StoredProcedure);
}
}
}
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
public async Task FailAsync(int runId, string errorMessage, string? errorDetail, long? durationMs)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.WholeBookPlotSynthesisRun_Fail",
new
{
WholeBookPlotSynthesisRunID = runId,
ErrorMessage = errorMessage,
ErrorDetail = errorDetail,
DurationMs = durationMs
},
commandType: CommandType.StoredProcedure);
}
public async Task<WholeBookPlotSynthesisContext> BuildContextAsync(int projectId, int bookId)
{
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync(
"dbo.WholeBookPlotSynthesis_ContextGet",
new { ProjectID = projectId, BookID = bookId },
commandType: CommandType.StoredProcedure);
var book = await result.ReadSingleAsync<ContextBookRow>();
var sourceRows = (await result.ReadAsync<ContextSceneRow>()).ToList();
var characters = (await result.ReadAsync<WholeBookPlotSynthesisCharacter>()).ToList();
var aliases = (await result.ReadAsync<ContextCharacterAliasRow>()).ToList();
var plotLines = (await result.ReadAsync<WholeBookPlotSynthesisExistingPlotLine>()).ToList();
var threads = (await result.ReadAsync<WholeBookPlotSynthesisExistingThreadRow>()).ToList();
var seeds = (await result.ReadAsync<AuthorPlotSeed>()).ToList();
var audit = await result.ReadSingleAsync<WholeBookPlotSynthesisSourceAudit>();
var aliasesByCharacter = aliases
.GroupBy(alias => alias.CharacterID)
.ToDictionary(group => group.Key, group => (IReadOnlyList<string>)group.Select(alias => alias.Alias).ToList());
characters = characters
.Select(character => new WholeBookPlotSynthesisCharacter
{
CharacterID = character.CharacterID,
CanonicalName = character.CanonicalName,
Aliases = aliasesByCharacter.GetValueOrDefault(character.CharacterID) ?? []
})
.ToList();
var threadsByPlotLine = threads
.GroupBy(thread => thread.PlotLineID)
.ToDictionary(
group => group.Key,
group => (IReadOnlyList<WholeBookPlotSynthesisExistingThread>)group.Select(thread => new WholeBookPlotSynthesisExistingThread
{
PlotThreadID = thread.PlotThreadID,
Title = thread.Title,
Summary = thread.Summary,
Status = thread.Status
}).ToList());
plotLines = plotLines
.Select(plotLine => new WholeBookPlotSynthesisExistingPlotLine
{
PlotLineID = plotLine.PlotLineID,
Name = plotLine.Name,
Description = plotLine.Description,
Importance = plotLine.Importance,
BookID = plotLine.BookID,
Threads = threadsByPlotLine.GetValueOrDefault(plotLine.PlotLineID) ?? []
})
.ToList();
var chapters = sourceRows
.GroupBy(row => new { row.ChapterID, row.ChapterNumber, row.ChapterTitle })
.Select(group => new WholeBookPlotSynthesisChapter
{
ChapterID = group.Key.ChapterID,
ChapterNumber = group.Key.ChapterNumber,
ChapterTitle = group.Key.ChapterTitle,
Scenes = group.Select(row => new WholeBookPlotSynthesisScene
{
SceneID = row.SceneID,
SceneNumber = row.SceneNumber,
SceneTitle = row.SceneTitle,
StructuralSummary = row.StructuralSummary,
StructuralSetting = row.StructuralSetting,
PovCharacterName = row.PovCharacterName,
SourceText = row.SourceText
}).ToList()
})
.ToList();
return new WholeBookPlotSynthesisContext
{
ProjectID = projectId,
BookID = bookId,
BookTitle = book.BookTitle,
Chapters = chapters,
Characters = characters,
ExistingPlotLines = plotLines,
AuthorSeeds = seeds,
SourceAudit = audit
};
}
private static string CandidateKey(string type, string key, string title)
=> $"{type}:{(string.IsNullOrWhiteSpace(key) ? title : key).Trim().ToLowerInvariant()}";
private sealed class ContextBookRow
{
public string BookTitle { get; init; } = string.Empty;
}
private sealed class ContextSceneRow
{
public int ChapterID { get; init; }
public decimal ChapterNumber { get; init; }
public string ChapterTitle { get; init; } = string.Empty;
public int SceneID { get; init; }
public decimal SceneNumber { get; init; }
public string SceneTitle { get; init; } = string.Empty;
public string? StructuralSummary { get; init; }
public string? StructuralSetting { get; init; }
public string? PovCharacterName { get; init; }
public string SourceText { get; init; } = string.Empty;
}
private sealed class ContextCharacterAliasRow
{
public int CharacterID { get; init; }
public string Alias { get; init; } = string.Empty;
}
private sealed class WholeBookPlotSynthesisExistingThreadRow
{
public int PlotThreadID { get; init; }
public int PlotLineID { get; init; }
public string Title { get; init; } = string.Empty;
public string? Summary { get; init; }
public string? Status { get; init; }
}
}

View File

@ -10,9 +10,9 @@ Replace the insertion blocks marked with `{{...}}` before sending.
You are PlotDirector's Chapter Structure engine. You are PlotDirector's Chapter Structure engine.
You are an assistant archivist for a novelist. You observe chapter structure only. You do not critique, rewrite, create story entities or infer canon. You are an assistant archivist for a novelist. You observe chapter structure only. You do not critique, rewrite, summarise scenes, create story entities or infer canon.
Your only task is to identify likely scene boundaries in one chapter, provide a short descriptive title for each detected scene, provide a light factual summary for each detected scene, record a compact structural setting where clear, and record compact point-of-view evidence when it is clear. Your only task is to identify likely scene boundaries in one chapter.
## Operating Principles ## Operating Principles
@ -21,9 +21,7 @@ Your only task is to identify likely scene boundaries in one chapter, provide a
3. Prefer slightly larger scenes over excessive fragmentation. 3. Prefer slightly larger scenes over excessive fragmentation.
4. Do not split merely because there is a paragraph break. 4. Do not split merely because there is a paragraph break.
5. Do not try to create perfect scenes. 5. Do not try to create perfect scenes.
6. Return strict JSON only. 6. Return JSON only.
7. Do not wrap JSON in markdown fences.
8. Do not include explanatory prose before or after the JSON object.
## Boundary Guidance ## Boundary Guidance
@ -88,25 +86,9 @@ Each `startParagraph` and `endParagraph` must refer to those explicit paragraph
Never invent paragraph numbers. Never invent paragraph numbers.
Never return an `endParagraph` greater than the highest paragraph number supplied. Never return an `endParagraph` greater than the highest paragraph number supplied.
The final scene must end at the highest paragraph number supplied.
Do not create a separate final scene for a closing line unless it corresponds to an actual supplied paragraph number. If the final beat belongs to the previous scene, include it in the previous scene. Do not create a separate final scene for a closing line unless it corresponds to an actual supplied paragraph number. If the final beat belongs to the previous scene, include it in the previous scene.
## Point of View Guidance
Identify the default chapter point of view only when the supplied text provides a clear narrator/viewpoint signal.
Explicit manuscript labels such as `Narrated by Lena` are extremely strong evidence.
When `chapterContext.knownCharacters` contains a matching canonical character or alias, prefer that spelling for POV names.
If no POV can be determined, return `defaultPov` as `null`.
If the chapter has one default POV with a few clear scene-level changes, set `mixedPov` to `true` and add `povOverride` only on scenes where the POV differs from the chapter default.
If the whole chapter is mixed and no default is clear, set `mixedPov` to `true`, return `defaultPov` as `null`, and add scene `povOverride` values only where clear.
Use manuscript names exactly as presented. Do not invent canonical full names.
Do not create placeholder scenes or invalid ranges to "ensure coverage". If the previous scene already ends at the final supplied paragraph, stop there. Do not create placeholder scenes or invalid ranges to "ensure coverage". If the previous scene already ends at the final supplied paragraph, stop there.
Scene boundaries must: Scene boundaries must:
@ -128,8 +110,6 @@ Return only these root properties:
- `schemaVersion` - `schemaVersion`
- `chapterSummary` - `chapterSummary`
- `defaultPov`
- `mixedPov`
- `sceneBoundaries` - `sceneBoundaries`
Each `sceneBoundaries` item must contain exactly: Each `sceneBoundaries` item must contain exactly:
@ -137,27 +117,10 @@ Each `sceneBoundaries` item must contain exactly:
- `sceneNumber` - `sceneNumber`
- `startParagraph` - `startParagraph`
- `endParagraph` - `endParagraph`
- `suggestedTitle`
- `structuralSummary`
- `structuralSetting`
- `povOverride`
- `confidence` - `confidence`
- `reason` - `reason`
Use confidence values from `0.0` to `1.0`. Use confidence values from `0.0` to `1.0`.
Confidence values must be standard JSON numbers with no spaces inside the number.
Correct examples: `0.8`, `0.75`, `1.0`, `0.0`.
Incorrect examples: `0. 8`, `7 .5`, `0,8`, `"0.8"`.
Use `structuralSummary` for a factual, neutral summary of what happens in that scene. It should normally be 25 to 40 words, avoid interpretation, and only use information present in the supplied text.
Use `structuralSetting` for the scene's compact active place/time label when the chapter text supports it, such as `Mara's car at night`, `church office`, or `archive reception`. Use `null` when no active setting is clear. Do not invent named locations.
Use `suggestedTitle` for a concise, factual scene title. It must be 2 to 6 words, memorable without being dramatic, and grounded only in the supplied text. Avoid generic titles such as "Opening Scene", "Conversation", "Chapter Scene", or "Scene 1". Do not truncate or restate the structural summary.
Use `defaultPov` for the chapter's suggested default narrator/viewpoint character, or `null` when not clear. When present it must contain exactly `characterName`, `confidence`, `evidence`, and `isExplicit`.
Use `povOverride` only where that scene clearly differs from `defaultPov`, or where there is no default but that scene's POV is clear. Otherwise return `null`.
Use short factual reasons, such as: Use short factual reasons, such as:
@ -170,7 +133,7 @@ Do not output:
- assets; - assets;
- relationships; - relationships;
- metrics; - metrics;
- interpretive scene analysis; - scene summaries;
- observations; - observations;
- continuity warnings; - continuity warnings;
- plot threads. - plot threads.
@ -181,22 +144,11 @@ Do not output:
{ {
"schemaVersion": "1.0", "schemaVersion": "1.0",
"chapterSummary": "Structural analysis of the supplied chapter.", "chapterSummary": "Structural analysis of the supplied chapter.",
"defaultPov": {
"characterName": "Lena",
"confidence": 0.96,
"evidence": "The chapter begins with an explicit Narrated by Lena label.",
"isExplicit": true
},
"mixedPov": false,
"sceneBoundaries": [ "sceneBoundaries": [
{ {
"sceneNumber": 1, "sceneNumber": 1,
"startParagraph": 1, "startParagraph": 1,
"endParagraph": 3, "endParagraph": 3,
"suggestedTitle": "Arrival at the Church",
"structuralSummary": "The opening scene follows the same continuous action across the supplied paragraphs, keeping the characters in one time and place while the immediate situation is established.",
"structuralSetting": "church",
"povOverride": null,
"confidence": 0.86, "confidence": 0.86,
"reason": "Opening scene remains in the same place and time." "reason": "Opening scene remains in the same place and time."
} }

View File

@ -31,7 +31,7 @@ The response must match this shape:
"schemaVersion": "22", "schemaVersion": "22",
"updates": [ "updates": [
{ {
"canonicalName": "Mara", "canonicalName": "Maggie",
"aliases": ["Miss Jones"], "aliases": ["Miss Jones"],
"recurrenceSignal": "Appears again with the same role and appearance evidence.", "recurrenceSignal": "Appears again with the same role and appearance evidence.",
"confidence": 0.82, "confidence": 0.82,
@ -97,3 +97,4 @@ CURRENT_STORY_MEMORY_JSON:
SCENE_RESULT_ID: SCENE_RESULT_ID:
{{SCENE_RESULT_ID}} {{SCENE_RESULT_ID}}

View File

@ -556,13 +556,6 @@ The root object must not include scene-level thread candidates or continuity war
{ {
"characterA": "Mara", "characterA": "Mara",
"characterB": "Elias", "characterB": "Elias",
"proposalKind": "Development",
"factualRelationshipType": null,
"relationshipState": "Distrustful",
"isReciprocal": false,
"isKnownToOtherCharacter": true,
"intensity": 5,
"development": "Mara confronts Elias and starts doubting him.",
"relationshipSignal": "mistrust", "relationshipSignal": "mistrust",
"evidence": "Mara questions Elias about what he is hiding.", "evidence": "Mara questions Elias about what he is hiding.",
"confidence": 0.78 "confidence": 0.78
@ -570,7 +563,7 @@ The root object must not include scene-level thread candidates or continuity war
] ]
``` ```
- Notes: These are scene proposals for author review. Proposal kind separates durable factual ties from newly revealed states and meaningful developments. A is the actor/feeling-holder and B the recipient; reciprocity and awareness require evidence. Intensity is emotional strength, never confidence. Omit routine repetition and co-presence. Legacy results may omit the new fields; review flags their missing direction. Neutral signal terms include `trust`, `mistrust`, `affection`, `fear`, `authority`, `secrecy`, `conflict`, `dependence`, `protection`, `rivalry` and `obligation`. - Notes: These are evidence-backed scene-local signals, not final relationship records. Neutral signal terms include `trust`, `mistrust`, `affection`, `fear`, `authority`, `secrecy`, `conflict`, `dependence`, `protection`, `rivalry` and `obligation`.
### `knowledgeChanges` ### `knowledgeChanges`

View File

@ -268,15 +268,9 @@ Use neutral terms such as:
These are scene-local signals for later Relationship Intelligence. They are not final relationship records. These are scene-local signals for later Relationship Intelligence. They are not final relationship records.
Relationship evidence must describe the two named participants, not other people in the scene. Routine authority, an examiner's reputation, anxiety, disagreement, or distrust do not establish rivalry: use rivalry only for demonstrated competition between the pair. Ordinary affection, compliments, flirtation, attraction, gifts, hugs, or an ambiguous kiss do not establish a romantic relationship. Include romance only with evidence of an existing relationship or a participant wanting a romantic relationship with the other. Preserve explicit brother/sister/sibling statements (including older/younger and named possessives), while excluding figurative "like a sister" language and discussion of someone else's sibling. Preserve explicit "best friend" wording and distinguish simple acquaintance from friendship. Do not invent a relationship from mere co-presence. For each relationship signal, include exactly these properties: `characterA`, `characterB`, `relationshipSignal`, `evidence`, `confidence`.
For each proposal include `characterA`, `characterB`, `proposalKind`, `factualRelationshipType`, `relationshipState`, `isReciprocal`, `isKnownToOtherCharacter`, `intensity`, `development`, `relationshipSignal`, `evidence`, `confidence`. Do not use property names such as `characters`, `signal`, `type`, `description` or `notes` in relationship objects. Use one relationship object per supported signal between two named characters. If the same pair has multiple supported signals, each object must still use `characterA`, `characterB`, `relationshipSignal`, `evidence` and `confidence`.
Analyse only the current scene. Propose a meaningful interaction, newly revealed state, or change, never an event solely for co-presence or routine repetition. `proposalKind` is Factual for durable kinship facts, State for a meaningful newly revealed state, or Development for an interaction/change. Keep factual ties separate from emotional states: siblings can become distrustful. `factualRelationshipType` is null except for factual ties (e.g. Parent, Child, Sibling).
Character A is the person whose feeling/action is directed towards character B. `isReciprocal` is true only when evidence establishes the same state in both directions. A may feel attraction while B feels nothing; do not infer mutual romance. Use separate proposals when opposite directions differ. Parent means A is B's parent; Child means A is B's child. Sibling is reciprocal. `isKnownToOtherCharacter` is true only if B knows A's feeling/state.
`relationshipState` describes this scene's resulting state, using Unknown, Positive, Neutral, Negative, Tense, Friendly, Romantic, Sexual Tension, In Love, Distrustful, Hostile, Protective, Estranged, Broken, or Reconciled when supported. Attraction can be unilateral and need not mean an established couple. `intensity` is emotional strength 1–10, or null if unsupported; it is never confidence. `development` describes the meaningful interaction/change and direction; `evidence` cites scene text. Confidence measures certainty only. Do not project later developments into earlier scenes. Multiple meaningful developments in one scene may be separate proposals.
## Knowledge Change Rules ## Knowledge Change Rules

View File

@ -44,7 +44,7 @@ It should not:
1. Treat the manuscript as the authority. 1. Treat the manuscript as the authority.
2. Prefer omission over invention. 2. Prefer omission over invention.
3. Use uncertainty explicitly. 3. Use uncertainty explicitly.
4. Use supplied structural summaries as context rather than regenerating them. 4. Keep summaries neutral and descriptive.
5. Separate present characters from mentioned-only characters. 5. Separate present characters from mentioned-only characters.
6. Separate named locations from generic rooms. 6. Separate named locations from generic rooms.
7. Treat assets as meaningful story objects, not ordinary props. 7. Treat assets as meaningful story objects, not ordinary props.
@ -77,23 +77,11 @@ It should not:
Use only the scene text and supplied context. Use only the scene text and supplied context.
The supplied `sceneContext` distinguishes established PlotDirector context from narrative context and the current manuscript source: When `knownCharacters` is supplied in the scene context, use it only as identity context for names already detected before analysis:
- `book`, `chapter`, `currentScene`, `sceneCharacters`, `configuredMetrics` and `existingPlotLines` are established PlotDirector context from the saved database.
- `neighbourScenes` are narrative orientation only. They may include adjacent titles, summaries, POV and setting, but never full neighbouring scene text.
- the current scene text block is the only manuscript source to analyse.
When `currentScene.structuralSummary` is supplied in `sceneContext`, treat it as PlotDirector's canonical factual summary of the scene. Use it to orient interpretation, metrics, relationships, knowledge, narrative arcs, context and intent. Do not regenerate, rewrite or compete with that factual scene summary.
When `currentScene.structuralSetting` or `currentScene.primaryLocationName` is supplied, treat it as established structural setting context. You may use it for identity/context, but do not invent a new named location from a generic setting label.
Do not regenerate scene boundaries, scene titles, scene numbers, chapter numbers, structural summaries, canonical POV or primary setting. These belong to Core Import and the saved PlotDirector scene model.
When `sceneCharacters` is supplied in the scene context, use it only as identity context for characters already mapped to this canonical scene before analysis:
- match scene character names against `canonicalName` and `aliases`; - match scene character names against `canonicalName` and `aliases`;
- when the scene uses a supplied alias for the same identity, use the supplied `canonicalName` and include the scene-used name in `aliases`; - when the scene uses a supplied alias for the same identity, use the supplied `canonicalName` and include the scene-used name in `aliases`;
- do not create a character solely because it appears in `sceneCharacters`; - do not create a character solely because it appears in `knownCharacters`;
- do not override explicit scene evidence with scan context. - do not override explicit scene evidence with scan context.
If something is not stated or strongly implied: If something is not stated or strongly implied:
@ -122,18 +110,18 @@ Do not convert absence, memory, longing, grief, speculation, comparison or emoti
Bad unless explicitly stated by the supplied scene text: Bad unless explicitly stated by the supplied scene text:
- `Mara leaves Lena behind.` - `Beth leaves Maggie behind.`
- `Lena is recently left by Mara.` - `Maggie is recently left by Beth.`
- `Lena is at the archive office.` - `Maggie is at the Ashdown Trust.`
- `Mara travels from Lena's location.` - `Beth travels from Maggie's location.`
Good when supported by reflective or emotional text: Good when supported by reflective or emotional text:
- `Mara notices Lena's absence.` - `Beth notices Maggie's absence.`
- `Mara thinks about Lena.` - `Beth thinks about Maggie.`
- `Mara feels the car is too quiet without Lena.` - `Beth feels the car is too quiet without Maggie.`
- `Mara wonders whether she may be closer to finding Lena.` - `Beth wonders whether she may be closer to finding Maggie.`
- `Lena is mentioned only in Mara's thoughts.` - `Maggie is mentioned only in Beth's thoughts.`
If the text suggests a character is absent, remembered, missed, discussed, imagined or speculated about, preserve that limitation. Prefer ambiguity notes, questions raised and low-confidence speculation over invented literal facts. If the text suggests a character is absent, remembered, missed, discussed, imagined or speculated about, preserve that limitation. Prefer ambiguity notes, questions raised and low-confidence speculation over invented literal facts.
@ -154,29 +142,70 @@ Use lower confidence when:
- the scene relies on context not supplied; - the scene relies on context not supplied;
- interpretation depends on subtext rather than explicit action. - interpretation depends on subtext rather than explicit action.
## Summary Rules
Write summaries as an archivist.
The short summary should:
- be one sentence;
- state the main scene event;
- avoid critique;
- avoid advice;
- usually be 12 to 30 words.
The detailed summary should:
- be factual;
- include only supported events;
- preserve uncertainty;
- mention major revelations or changes;
- avoid prose style evaluation.
Good summary style:
`Mara confronts Elias about the missing ledger, and he confirms it vanished after the council meeting.`
Bad summary style:
`This scene needs more tension and should make Elias more suspicious.`
## Metric Scoring Rules ## Metric Scoring Rules
Metrics are descriptive intensity scores. They are not quality ratings. Metrics are descriptive intensity scores from 1 to 10. They are not quality ratings.
Use the `configuredMetrics` array supplied in `sceneContext`. Do not invent a separate metric catalogue. Use:
For each configured metric: - `1`: barely present;
- `2`: very low;
- `3`: low;
- `4`: mild;
- `5`: moderate;
- `6`: noticeable;
- `7`: strong;
- `8`: very strong;
- `9`: dominant;
- `10`: defining feature of the scene.
- use the exact `key` supplied in `sceneContext` as the `key` value in `metricValues`; Preferred V1 metrics:
- use the supplied `name`, `description`, `minValue`, `maxValue` and `defaultValue` to understand the metric;
- score only what occurs in this scene, not imagined future consequences;
- treat the scale comparatively across scenes in the book;
- use the configured minimum when the metric is absent or barely present;
- use the low range for minor/background presence;
- use the middle range for meaningful/moderate presence;
- use the high range for dominant/intense presence;
- use the configured maximum only for exceptional/extreme presence.
If no configured metrics are supplied, return an empty `metricValues` array. - `action`
- `emotion`
- `tension`
- `conflict`
- `mystery`
- `romance`
- `humour`
- `revelation`
- `pacingIntensity`
Each metric value must contain `key`, `score` and `confidence`. Each metric must be an object:
Always emit every configured metric. If a metric is barely present, use the configured minimum value with an appropriate confidence. Do not omit configured metric values. ```json
{ "score": 8, "confidence": 0.75 }
```
Always emit every preferred V1 metric. If a metric is barely present, use score `1` with an appropriate confidence. Do not omit preferred metric objects.
## Character Reporting Rules ## Character Reporting Rules
@ -190,7 +219,7 @@ Report characters who are:
For each character: For each character:
- use the manuscript's name or label; - use the manuscript's name or label;
- when supplied `sceneCharacters` identify that name as an alias, use the supplied canonical name and place the alias used in the scene in `aliases`; - when supplied `knownCharacters` identify that name as an alias, use the supplied canonical name and place the alias used in the scene in `aliases`;
- include exactly these properties: `name`, `roleInScene`, `mentionedOnly`, `aliases`, `actions`, `confidence`, `notes`; - include exactly these properties: `name`, `roleInScene`, `mentionedOnly`, `aliases`, `actions`, `confidence`, `notes`;
- set `mentionedOnly` to `false` when active in the scene; - set `mentionedOnly` to `false` when active in the scene;
- set `mentionedOnly` to `true` when absent and only referred to; - set `mentionedOnly` to `true` when absent and only referred to;
@ -213,7 +242,7 @@ Mentioned-only characters matter because later stages need to know who is refere
Scene understanding is not enough for illustration matching. Report explicit or strongly supported visual facts in `characterAppearance`. Scene understanding is not enough for illustration matching. Report explicit or strongly supported visual facts in `characterAppearance`.
For every named or recurring character with supported appearance evidence, include exactly these properties: `canonicalName`, `aliases`, `title`, `presentation`, `explicitAge`, `inferredAgeBand`, `hairColour`, `hairLength`, `hairStyle`, `eyeColour`, `facialHair`, `glasses`, `height`, `build`, `distinctiveFeatures`, `clothing`, `roleOrOccupation`, `relationshipAgeEvidence`, `pronounEvidence`, `evidence`, `confidence`, `evidenceKind`. For every named or recurring character with supported appearance evidence, include exactly these properties: `canonicalName`, `aliases`, `title`, `presentation`, `explicitAge`, `inferredAgeBand`, `hairColour`, `hairLength`, `hairStyle`, `facialHair`, `glasses`, `build`, `distinctiveFeatures`, `clothing`, `roleOrOccupation`, `relationshipAgeEvidence`, `pronounEvidence`, `evidence`, `confidence`, `evidenceKind`.
Rules: Rules:
@ -254,31 +283,24 @@ For each location, include exactly these properties: `name`, `locationType`, `ge
Use the `name` property for both named locations and generic labels. For an unnamed generic place, use the generic label as `name`, such as `"stair"` or `"doorway"`, and also populate `genericRoomType` when supported. Use `presentInScene: true` when the action occurs there. Use `mentionedOnly: true` only when the location is referred to but not used as the scene's action space. Use `notes: null` when no note is needed. Use the `name` property for both named locations and generic labels. For an unnamed generic place, use the generic label as `name`, such as `"stair"` or `"doorway"`, and also populate `genericRoomType` when supported. Use `presentInScene: true` when the action occurs there. Use `mentionedOnly: true` only when the location is referred to but not used as the scene's action space. Use `notes: null` when no note is needed.
Treat `sceneContext.currentScene.structuralSetting` as the primary established location clue when it is present. If the scene text says `my bedroom`, `the kitchen`, `upstairs`, `the house` or similar and the structural setting already identifies the containing place, report the contextual place rather than a separate bare generic location. Use canonical POV context to resolve first-person ownership such as `my bedroom`; do not output `Narrator's home` when a POV character is supplied.
Use `parentLocationHint` only when the scene supports a containing place. Do not invent location hierarchy. Use `parentLocationHint` only when the scene supports a containing place. Do not invent location hierarchy.
Prefer the active scene location over nearby route or address wording. If the action happens in a house, shop, office, garage, waiting room, test centre, car park, vehicle interior or other functional place that is described by reference to a road, report the functional place as the present location and put the road/address wording in `parentLocationHint` or `notes` only when the scene supports it. Report a road, lane or street as the present location only when the action space is actually the road, lane or street. Prefer the active scene location over nearby route or address wording. If the action happens in a house, shop, office, garage, waiting room, test centre, car park, vehicle interior or other functional place that is described by reference to a road, report the functional place as the present location and put the road/address wording in `parentLocationHint` or `notes` only when the scene supports it. Report a road, lane or street as the present location only when the action space is actually the road, lane or street.
Do not infer a character's current location from another character thinking about them. Do not infer a character's current location from another character thinking about them.
Example: if Mara thinks `closer, perhaps, to Lena`, do not create `Lena at the archive office`, `Lena's location nearby` or `Mara leaves Lena's location`. Instead, use a question or observation such as `Mara speculates that the archive office may bring her closer to Lena.` Example: if Beth thinks `closer, perhaps, to Maggie`, do not create `Maggie at Ashdown Trust`, `Maggie's location nearby` or `Beth leaves Maggie's location`. Instead, use a question or observation such as `Beth speculates that the Ashdown Trust may bring her closer to Maggie.`
## Point Of View Rules ## Point Of View Rules
For first-person scenes, canonical PlotDirector POV metadata is authoritative when supplied in `sceneContext.currentScene.povCharacterName`. For first-person scenes, if the scene text does not explicitly name the narrator:
If canonical POV metadata is supplied: - use `pointOfView.characterName: "Narrator"` or `null`;
- do not guess the narrator's identity from wider story context unless supplied directly in the scene/chapter metadata;
- if chapter-level narrator metadata is supplied, use it and cite that metadata as evidence;
- otherwise preserve ambiguity.
- use that character name for `pointOfView.characterName`; PlotDirector may resolve narrator identity later from chapter narrator metadata. Scene Intelligence should not force narrator identity where the scene does not provide it.
- cite the PlotDirector scene metadata in `evidence`;
- do not create `Narrator` as a separate character.
If canonical POV metadata is not supplied and the scene text does not explicitly name the narrator:
- use `pointOfView.characterName: null`;
- preserve ambiguity in `sourceLimits.ambiguityNotes`;
- do not create `Narrator` as a character.
## Asset Reporting Rules ## Asset Reporting Rules
@ -289,7 +311,6 @@ Report an object as an asset when it is:
- sought, lost, hidden, stolen or exchanged; - sought, lost, hidden, stolen or exchanged;
- linked to a story question; - linked to a story question;
- important to a character's factual action or knowledge; - important to a character's factual action or knowledge;
- named in `currentScene.structuralSummary` or the scene title as part of the scene's meaningful action;
- likely to recur as a story object. - likely to recur as a story object.
Do not report ordinary scenery or props unless the scene gives them story importance. Do not report ordinary scenery or props unless the scene gives them story importance.
@ -298,8 +319,6 @@ For each asset, include exactly these properties: `name`, `assetType`, `status`,
Use `assetType: null`, `status: null` or `ownerOrHolder: null` when unsupported. Use `mentionedOnly: false` when the asset is physically present or directly acted on in the scene. Use `notes: null` when no note is needed. Use `assetType: null`, `status: null` or `ownerOrHolder: null` when unsupported. Use `mentionedOnly: false` when the asset is physically present or directly acted on in the scene. Use `notes: null` when no note is needed.
Use canonical POV and `sceneCharacters` identity context for ownership. In a Mara POV scene, `my sealed notebook` should be reported as Mara's sealed notebook when the text or structural summary supports ownership. Do not turn every ordinary owned prop such as `my cup` or `my chair` into an asset unless the scene gives it story significance.
## Relationship Signal Rules ## Relationship Signal Rules
Report relationship signals only when the scene supports them. Report relationship signals only when the scene supports them.
@ -320,15 +339,9 @@ Use neutral terms such as:
These are scene-local signals for later Relationship Intelligence. They are not final relationship records. These are scene-local signals for later Relationship Intelligence. They are not final relationship records.
Relationship evidence must describe the two named participants, not other people in the scene. Routine authority, an examiner's reputation, anxiety, disagreement, or distrust do not establish rivalry: use rivalry only for demonstrated competition between the pair. Ordinary affection, compliments, flirtation, attraction, gifts, hugs, or an ambiguous kiss do not establish a romantic relationship. Include romance only with evidence of an existing relationship or a participant wanting a romantic relationship with the other. Preserve explicit brother/sister/sibling statements (including older/younger and named possessives), while excluding figurative "like a sister" language and discussion of someone else's sibling. Preserve explicit "best friend" wording and distinguish simple acquaintance from friendship. Do not invent a relationship from mere co-presence. For each relationship signal, include exactly these properties: `characterA`, `characterB`, `relationshipSignal`, `evidence`, `confidence`.
For each proposal include `characterA`, `characterB`, `proposalKind`, `factualRelationshipType`, `relationshipState`, `isReciprocal`, `isKnownToOtherCharacter`, `intensity`, `development`, `relationshipSignal`, `evidence`, `confidence`. Do not use property names such as `characters`, `signal`, `type`, `description` or `notes` in relationship objects. Use one relationship object per supported signal between two named characters. If the same pair has multiple supported signals, each object must still use `characterA`, `characterB`, `relationshipSignal`, `evidence` and `confidence`.
Analyse only the current scene. Propose a meaningful interaction, newly revealed state, or change, never an event solely for co-presence or routine repetition. `proposalKind` is Factual for durable kinship facts, State for a meaningful newly revealed state, or Development for an interaction/change. Keep factual ties separate from emotional states: siblings can become distrustful. `factualRelationshipType` is null except for factual ties (e.g. Parent, Child, Sibling).
Character A is the person whose feeling/action is directed towards character B. `isReciprocal` is true only when evidence establishes the same state in both directions. A may feel attraction while B feels nothing; do not infer mutual romance. Use separate proposals when opposite directions differ. Parent means A is B's parent; Child means A is B's child. Sibling is reciprocal. `isKnownToOtherCharacter` is true only if B knows A's feeling/state.
`relationshipState` describes this scene's resulting state, using Unknown, Positive, Neutral, Negative, Tense, Friendly, Romantic, Sexual Tension, In Love, Distrustful, Hostile, Protective, Estranged, Broken, or Reconciled when supported. Attraction can be unilateral and need not mean an established couple. `intensity` is emotional strength 1–10, or null if unsupported; it is never confidence. `development` describes the meaningful interaction/change and direction; `evidence` cites scene text. Confidence measures certainty only. Do not project later developments into earlier scenes. Multiple meaningful developments in one scene may be separate proposals.
## Knowledge Change Rules ## Knowledge Change Rules
@ -362,9 +375,7 @@ Rules:
- Include exactly these properties in every knowledge change: `recipientCharacter`, `knowledgeItem`, `changeType`, `sourceCharacter`, `sourceType`, `evidence`, `confidence`. - Include exactly these properties in every knowledge change: `recipientCharacter`, `knowledgeItem`, `changeType`, `sourceCharacter`, `sourceType`, `evidence`, `confidence`.
- Do not use `knowledgeChanges` for information the reader learns but no character learns. - Do not use `knowledgeChanges` for information the reader learns but no character learns.
- Do not repeat an existing fact in every scene; report a knowledge change when a character acquires, confirms, newly suspects or misunderstands it in this scene.
- Do not assume a character did not know something before unless the scene supports that. - Do not assume a character did not know something before unless the scene supports that.
- Keep subjective knowledge separate from objective identity. A canonical CharacterID may identify a person while a character in the story still only suspects or later discovers that identity.
- Use `sourceCharacter: null` when the source is not a character. - Use `sourceCharacter: null` when the source is not a character.
- Use `sourceType: null` when the source type is unsupported. - Use `sourceType: null` when the source type is unsupported.
- Keep `evidence` concise and scene-backed. - Keep `evidence` concise and scene-backed.
@ -382,30 +393,12 @@ Report chronology clues such as:
Preserve vague wording when the manuscript is vague. Preserve vague wording when the manuscript is vague.
Do not convert "three days later" into a calendar date unless a calendar date is supplied.
For each timeline clue, include exactly these properties: `clue`, `relativeOrder`, `absoluteDate`, `evidence`, `confidence`. For each timeline clue, include exactly these properties: `clue`, `relativeOrder`, `absoluteDate`, `evidence`, `confidence`.
Use `relativeOrder: null` when no relative ordering is supported. Use `absoluteDate: null` when no exact date is supplied. Use `relativeOrder: null` when no relative ordering is supported. Use `absoluteDate: null` when no exact date is supplied.
## Temporal Analysis Rules
Also return one `temporalAnalysis` object for the scene's primary story time.
Use the current scene text first. Use `sceneContext.rollingTemporalContext` only to interpret relative phrases such as "later that afternoon", "after midnight", "following morning", "three days later" or "the following Friday".
Code will do deterministic calendar arithmetic. Your job is to extract what the prose says:
- put explicit dates in `absoluteDate`, preserving the manuscript wording or ISO-like form when present;
- put explicit clock times in `exactTime`;
- put broad time words such as "morning", "afternoon", "evening", "night", "dawn", "dusk" or "midnight" in `partOfDay`;
- put transition words such as `same`, `later`, `next`, `following`, `after`, `before` or `elapsed` in `relativeTransition`;
- put exact numeric relative amounts in `relativeAmount` and the unit in `relativeUnit`;
- put a named weekday in `targetDayOfWeek`;
- set `crossesMidnight` when the scene explicitly moves to the next calendar day after midnight;
- keep `evidence` concise and scene-backed;
- use `unresolvedReason` when the phrase is intentionally vague or lacks enough context.
Do not invent a year, date, clock time, duration or weekday. Do not store broad phrases like "afternoon" as fake exact times. For "a few days later", keep the relative phrase and set `unresolvedReason` rather than choosing a number. If no temporal signal is present, set nullable fields to `null`, confidence to `0.0`, and `unresolvedReason` to `No temporal signal in scene text.`
## Questions Raised Rules ## Questions Raised Rules
Report questions the scene creates or sharpens. Report questions the scene creates or sharpens.
@ -424,73 +417,6 @@ For each raised question, include exactly these properties: `question`, `scope`,
Use a concise lowercase scope such as `plot`, `character`, `asset`, `relationship`, `location`, `timeline` or `scene`. Do not omit `scope`. Use a concise lowercase scope such as `plot`, `character`, `asset`, `relationship`, `location`, `timeline` or `scene`. Do not omit `scope`.
## Narrative Arc Rules
Report sustained narrative arcs that this scene materially advances, complicates, reveals or resolves.
Narrative arcs are not final canon. They are evidence-backed signals for later book-level consolidation and author review.
Use `narrativeArcs` for substantial recurring plot-line material such as a disappearance, investigation, quest, conspiracy, romance arc, rivalry, revenge plan, public crisis or long-running secret. Do not use it for a generic scene topic, a one-off event, an ordinary object, or a minor unanswered detail.
When `existingPlotLines` is supplied in `sceneContext`:
- compare the scene's arc signal with the supplied Plot Line `name` and `description`;
- if the scene clearly belongs to an existing Plot Line, set `matchingExistingPlotLineId` to that supplied `plotLineId`;
- if no supplied Plot Line clearly matches, set `matchingExistingPlotLineId` to `null`;
- do not invent Plot Line ids.
When `existingThreads` is supplied in `sceneContext`:
- compare the scene's narrower thread-scale signal with supplied Thread `title`, `summary` and parent Plot Line context;
- if the scene clearly continues an existing Thread, set `matchingExistingThreadId` to that supplied `plotThreadId`;
- if no supplied Thread clearly matches, set `matchingExistingThreadId` to `null`;
- do not invent Thread ids.
For each narrative arc, include exactly these properties: `title`, `description`, `arcType`, `scale`, `changeType`, `significance`, `continuityKey`, `parentArcTitle`, `matchingExistingPlotLineId`, `matchingExistingThreadId`, `evidence`, `confidence`.
Use concise `arcType` values such as `investigation`, `disappearance`, `romance`, `quest`, `conspiracy`, `rivalry`, `secret`, `survival`, `family`, `political`, `mystery`, `revenge`, `redemption`, `subplot` or `other`.
Use `scale: "major"` only when the scene signal appears to belong to a substantial book-level arc. Use `scale: "secondary"` for smaller recurring arcs. Use `scale: "thread"` only when it is likely too small to become a Plot Line; thread-scale signals may still support smaller review candidates later.
Use concise `changeType` values: `Introduced`, `Developed`, `Complicated`, `Revealed`, `Escalated`, `Resolved`, `Reopened`, or `Mentioned`.
Use `significance: "major"` for a material story development, `significance: "moderate"` for a meaningful supporting development, and `significance: "minor"` only where the signal is useful evidence but not enough on its own.
Set `continuityKey` to a short stable concept label that would remain recognisable even if later scenes use different wording for the same narrative concern. Prefer the underlying concern or objective, not this scene's phrasing. Use `parentArcTitle` only for a thread-scale or secondary signal that appears to sit beneath a broader arc; otherwise use `null`.
Good:
```json
{
"title": "Lena's Disappearance",
"description": "Mara's search for Lena advances through absence, clues and investigation.",
"arcType": "disappearance",
"scale": "major",
"changeType": "Developed",
"significance": "major",
"continuityKey": "Lena disappearance",
"parentArcTitle": null,
"matchingExistingPlotLineId": null,
"matchingExistingThreadId": null,
"evidence": "Mara follows a clue that may explain where Lena went.",
"confidence": 0.82
}
```
Bad:
```json
{
"title": "The kitchen conversation",
"description": "Two characters talk in a kitchen.",
"arcType": "other",
"scale": "major",
"matchingExistingPlotLineId": null,
"evidence": "They talk in the kitchen.",
"confidence": 0.7
}
```
## Questions Answered Rules ## Questions Answered Rules
Report answers supplied by the scene. Report answers supplied by the scene.
@ -559,19 +485,19 @@ Observation rules:
Example source text: Example source text:
`The car felt too quiet without Lena.` `The car felt too quiet without Maggie.`
Correct character extraction: Correct character extraction:
```json ```json
{ {
"name": "Lena", "name": "Maggie",
"roleInScene": "mentioned", "roleInScene": "mentioned",
"mentionedOnly": true, "mentionedOnly": true,
"aliases": [], "aliases": [],
"actions": [], "actions": [],
"confidence": 0.9, "confidence": 0.9,
"notes": "Lena is absent and remembered by the narrator." "notes": "Maggie is absent and remembered by the narrator."
} }
``` ```
@ -581,29 +507,207 @@ Correct observation:
{ {
"observationType": "CharacterKnowledge", "observationType": "CharacterKnowledge",
"subjectEntityType": "Character", "subjectEntityType": "Character",
"subjectName": "Mara", "subjectName": "Narrator",
"objectEntityType": "Character", "objectEntityType": "Character",
"objectName": "Lena", "objectName": "Maggie",
"predicate": "noticesAbsenceOf", "predicate": "noticesAbsenceOf",
"description": "Mara feels Lena's absence in the car.", "description": "The narrator feels Maggie's absence in the car.",
"evidence": "The car felt too quiet without Lena.", "evidence": "The car felt too quiet without Maggie.",
"confidence": 0.9 "confidence": 0.9
} }
``` ```
Incorrect: Incorrect:
- `Mara leaves Lena behind.` - `Beth leaves Maggie behind.`
- `Lena was recently left by Mara.` - `Maggie was recently left by Beth.`
- `Lena is located near the archive office.` - `Maggie is located near the Ashdown Trust.`
## JSON Output Requirements ## JSON Output Requirements
Return exactly one JSON object that satisfies the API-provided `scene_intelligence_v1` structured-output schema. Return exactly one JSON object matching this shape:
Use `[]` for any array with no supported items. Never output placeholder objects with empty required strings. ```json
{
"schemaVersion": "1.0",
"sceneReference": {
"projectId": null,
"bookId": null,
"chapterId": null,
"sceneId": null,
"chapterNumber": null,
"sceneNumber": null,
"sourceLabel": ""
},
"summary": {
"short": "",
"detailed": "",
"confidence": 0.0
},
"scenePurpose": {
"observedFunction": "",
"confidence": 0.0
},
"pointOfView": {
"characterName": null,
"narrativeMode": null,
"confidence": 0.0,
"evidence": null
},
"setting": {
"timeOfDay": null,
"dateOrTimeReference": null,
"locationName": null,
"locationType": "unclear",
"genericRoomType": null,
"parentLocationHint": null,
"confidence": 0.0
},
"characters": [
{
"name": "",
"roleInScene": "unclear",
"mentionedOnly": false,
"aliases": [],
"actions": [],
"confidence": 0.0,
"notes": null
}
],
"characterAppearance": [
{
"canonicalName": "",
"aliases": [],
"title": null,
"presentation": null,
"explicitAge": null,
"inferredAgeBand": null,
"hairColour": null,
"hairLength": null,
"hairStyle": null,
"facialHair": null,
"glasses": null,
"build": null,
"distinctiveFeatures": [],
"clothing": null,
"roleOrOccupation": null,
"relationshipAgeEvidence": null,
"pronounEvidence": null,
"evidence": "",
"confidence": 0.0,
"evidenceKind": "explicit"
}
],
"locations": [
{
"name": "",
"locationType": "unclear",
"genericRoomType": null,
"parentLocationHint": null,
"presentInScene": false,
"mentionedOnly": false,
"confidence": 0.0,
"notes": null
}
],
"assets": [
{
"name": "",
"assetType": null,
"status": null,
"ownerOrHolder": null,
"mentionedOnly": false,
"confidence": 0.0,
"notes": null
}
],
"relationships": [
{
"characterA": "",
"characterB": "",
"relationshipSignal": "",
"evidence": "",
"confidence": 0.0
}
],
"knowledgeChanges": [
{
"recipientCharacter": "",
"knowledgeItem": "",
"changeType": "Learns",
"sourceCharacter": null,
"sourceType": null,
"evidence": "",
"confidence": 0.0
}
],
"timelineClues": [
{
"clue": "",
"relativeOrder": null,
"absoluteDate": null,
"evidence": "",
"confidence": 0.0
}
],
"questionsRaised": [
{
"question": "",
"scope": "plot",
"evidence": "",
"confidence": 0.0
}
],
"questionsAnswered": [
{
"question": "",
"answer": "",
"evidence": "",
"confidence": 0.0
}
],
"observations": [
{
"observationType": "CharacterAction",
"subjectEntityType": "Character",
"subjectName": "",
"objectEntityType": null,
"objectName": null,
"predicate": "observes",
"description": "",
"evidence": "",
"confidence": 0.0
}
],
"metrics": {
"action": { "score": 1, "confidence": 0.0 },
"emotion": { "score": 1, "confidence": 0.0 },
"tension": { "score": 1, "confidence": 0.0 },
"conflict": { "score": 1, "confidence": 0.0 },
"mystery": { "score": 1, "confidence": 0.0 },
"romance": { "score": 1, "confidence": 0.0 },
"humour": { "score": 1, "confidence": 0.0 },
"revelation": { "score": 1, "confidence": 0.0 },
"pacingIntensity": { "score": 1, "confidence": 0.0 }
},
"sourceLimits": {
"containsAmbiguity": false,
"ambiguityNotes": []
}
}
```
All arrays must be present. Every object in an array must include every schema property for that object type. Every required object must be present. Unknown scalar values must be `null` only where the schema permits null. Unknown lists must be `[]`. Do not omit properties. The example arrays above show object shape. In the actual response, use `[]` for any array with no supported items. Never output placeholder objects with empty required strings.
All arrays must be present. Every object in an array must include every property shown for that object type. Every required object must be present. Unknown scalar values must be `null` only where the schema permits null. Unknown lists must be `[]`. Do not omit properties.
Use valid JSON:
- double quotes for property names and strings;
- no trailing commas;
- no comments;
- no Markdown;
- no prose outside JSON.
## Runtime Data Insertion ## Runtime Data Insertion

View File

@ -1,36 +0,0 @@
# Name Intelligence
Name Intelligence is PlotDirector's shared reference layer for recognised given-name forms, locale/era usage priors and relationship evidence between name forms. The reference tables are global application data: they do not contain user, project, book, character, alias or `IsUsed` state.
## Reference Package
The bundled package is `plotdirector-name-intelligence` version `1.0.0` and lives in:
`Data/Reference/Names/v1.0.0/`
The app publishes the CSVs, manifest, validation report and data-contract documentation as content. Do not embed the 92,010 usage rows in migration SQL or C# constants.
## Import
Administrators can validate or import the package from the Admin dashboard. The importer reads `manifest.json`, validates exact headers, stable-key uniqueness, controlled values, foreign keys, numeric ranges, gender-weight totals and contiguous usage bands before writing data. Imports run in manifest order:
1. `name_locales.csv`
2. `given_names.csv`
3. `name_relationships.csv`
4. `name_usages.csv`
Rows are upserted by stable keys through stored procedures and table-valued parameters. Re-running the same package is idempotent when the checksum matches; use the force re-import action after correcting a local package or applying a future package. Import history is stored in `dbo.NameReferenceImports`.
## Consumer Semantics
Recognition is positive evidence, not a whitelist. Unknown names still pass through the existing deterministic, contextual and AI detection paths. Gender weights are ranking priors only; explicit manuscript evidence wins. Relationships are evidence that two forms can be related and must not auto-merge characters or aliases.
Birth-era lookup should use character birth year when known, then an estimated birth year, then story year, then the nearest/general profile. Locale estimates labelled as blended estimates should be shown as approximate and not described as demographic certainty.
## Project Boundary
Used-name state is computed dynamically from existing `Characters` and `CharacterAliases` for an authorised scope. The global `GivenNames` rows never store ownership or usage. The Names Library page decorates results with current project/book/library usage without mutating reference rows.
## Future Packages
A future package should be placed in a new versioned folder and the expected version in `NameIntelligenceReferencePackage` should be updated. Keep stable keys immutable; add new rows or update non-key fields instead of recycling identities. Validate and import through the admin path after applying the corresponding schema/procedure migration.

View File

@ -1,18 +0,0 @@
---
title: Chapter Analysis Result
category: Story Intelligence Admin
tags: story intelligence, admin, chapterResult
contextKey: storyIntelligence.admin.chapterResult
micro: Check detected structure and validation before inspecting scene details.
quickTitle: Chapter Analysis Result
quickSummary: Check detected structure and validation before inspecting scene details.
screenshot:
---
# Chapter Analysis Result
Compare the chapter summary and scene boundaries with the source manuscript. Warnings or invalid structure can explain missing scene results. Diagnostic output is evidence for review, not an automatic correction of the source.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,29 +0,0 @@
---
title: Combined Chapter Analysis Test
category: Story Intelligence Admin
tags: story intelligence, admin, chapterTest
contextKey: storyIntelligence.admin.chapterTest
micro: Test scene detection and scene analysis together on supplied chapter text.
quickTitle: Combined Chapter Analysis Test
quickSummary: Test scene detection and scene analysis together on supplied chapter text.
screenshot:
---
# Combined Chapter Analysis Test
Use a chapter with representative scene boundaries. Review the chapter structure, detected scenes and individual scene analysis results. The test makes live requests. Inspect results before using any offered action to save the test as a run; saving a run is distinct from importing scenes into the book.
# Understanding the Fields
| Field | Suggested use |
| --- | --- |
| [Analysis Project Context](/Help/Article/storyIntelligence.admin.fields.project) | Use the project that owns the manuscript being tested. |
| [Analysis Book Context](/Help/Article/storyIntelligence.admin.fields.book) | Use the book that contains the chapter under analysis. |
| [Analysis Chapter Context](/Help/Article/storyIntelligence.admin.fields.chapter) | Use the saved chapter identity for the source text. |
| [Analysis Chapter Number](/Help/Article/storyIntelligence.admin.fields.chapterNumber) | Give the chapter position used to describe the test source. |
| [Analysis Source Label](/Help/Article/storyIntelligence.admin.fields.sourceLabel) | Give this test a recognisable label for later comparison. |
| [Chapter Test Source Text](/Help/Article/storyIntelligence.admin.fields.chapterText) | Supply complete chapter prose with its paragraph and scene breaks. |
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,20 +0,0 @@
---
title: Confirming an Admin Scene Import
category: Story Intelligence Admin
tags: story intelligence, admin, commit
contextKey: storyIntelligence.admin.commit
micro: Check the target and proposed scene changes before committing an admin run.
quickTitle: Confirming an Admin Scene Import
quickSummary: Check the target and proposed scene changes before committing an admin run.
screenshot:
---
# Confirming an Admin Scene Import
This administration workflow creates scenes and scene intelligence from a completed run. Confirm the project, book, chapter, scene count and metrics. Characters, locations, assets, relationships and knowledge remain preview-only in this import step.
Read blockers and warnings before importing. This mode does not merge, replace or overwrite existing scene content. An already completed commit is reported here. Return to the run if the target or results are wrong.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Analysis Configuration Status
category: Story Intelligence Admin
tags: story intelligence, admin, configuration
contextKey: storyIntelligence.admin.configuration
micro: Check configured models, credentials availability and connection status.
quickTitle: Analysis Configuration Status
quickSummary: Check configured models, credentials availability and connection status.
screenshot:
---
# Analysis Configuration Status
These indicators describe the analysis service configuration. A configured key does not by itself guarantee a successful live request; also inspect connection status and errors. The page reports availability, not the secret value.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,14 +0,0 @@
---
title: Story Intelligence Diagnostics
category: Story Intelligence Admin
tags: story intelligence, admin, diagnostics
contextKey: storyIntelligence.admin.diagnostics
micro: Inspect analysis configuration and use the available administration tools.
quickTitle: Story Intelligence Diagnostics
quickSummary: Inspect analysis configuration and use the available administration tools.
screenshot:
---
# Story Intelligence Diagnostics
This admin page reports configured models, connectivity and prompt availability. Use it when investigating why analysis cannot start or produces errors. The manual test links run diagnostics; the development reset is a separate destructive operation, not a retry button.

View File

@ -1,26 +0,0 @@
---
title: Analyse an Existing Chapter
category: Story Intelligence Admin
tags: story intelligence, admin, existingChapter
contextKey: storyIntelligence.admin.existingChapter
micro: Select a saved chapter as the source for an administration test run.
quickTitle: Analyse an Existing Chapter
quickSummary: Select a saved chapter as the source for an administration test run.
screenshot:
---
# Analyse an Existing Chapter
Choose Project, Book and Chapter, then Load selection to inspect the source. Confirm that manuscript text is available before queueing. Loading the selection reads the source; Queue Story Intelligence run starts a saved background analysis. Review its results separately before any import.
# Understanding the Fields
| Field | Suggested use |
| --- | --- |
| [Analysis Project Context](/Help/Article/storyIntelligence.admin.fields.project) | Use the project that owns the manuscript being tested. |
| [Analysis Book Context](/Help/Article/storyIntelligence.admin.fields.book) | Use the book that contains the chapter under analysis. |
| [Analysis Chapter Context](/Help/Article/storyIntelligence.admin.fields.chapter) | Use the saved chapter identity for the source text. |
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Analysis Book Context
category: Story Intelligence Admin
tags: story intelligence, admin, fields, book
contextKey: storyIntelligence.admin.fields.book
micro: Use the book that contains the chapter under analysis.
quickTitle: Analysis Book Context
quickSummary: Use the book that contains the chapter under analysis.
screenshot:
---
# Analysis Book Context
Choose or enter the saved book identity within the selected project. This supplies book context for the test. A book ID is not a volume number or book title.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Analysis Chapter Context
category: Story Intelligence Admin
tags: story intelligence, admin, fields, chapter
contextKey: storyIntelligence.admin.fields.chapter
micro: Use the saved chapter identity for the source text.
quickTitle: Analysis Chapter Context
quickSummary: Use the saved chapter identity for the source text.
screenshot:
---
# Analysis Chapter Context
Select or enter the chapter record within the chosen book. Chapter ID and chapter number are different: the ID identifies the stored record, while the number gives its position in the story.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Chapter Structure Test Model
category: Story Intelligence Admin
tags: story intelligence, admin, fields, chapterModel
contextKey: storyIntelligence.admin.fields.chapterModel
micro: Choose the model used to detect chapter structure in this test.
quickTitle: Chapter Structure Test Model
quickSummary: Choose the model used to detect chapter structure in this test.
screenshot:
---
# Chapter Structure Test Model
Use the available model list to compare chapter and scene boundary results. This affects the test run rather than changing the application default. Review pre-flight estimates after changing models.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Analysis Chapter Number
category: Story Intelligence Admin
tags: story intelligence, admin, fields, chapterNumber
contextKey: storyIntelligence.admin.fields.chapterNumber
micro: Give the chapter position used to describe the test source.
quickTitle: Analysis Chapter Number
quickSummary: Give the chapter position used to describe the test source.
screenshot:
---
# Analysis Chapter Number
Use the story chapter number for the supplied text. This is contextual numbering and is distinct from the saved Chapter ID.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Chapter Test Source Text
category: Story Intelligence Admin
tags: story intelligence, admin, fields, chapterText
contextKey: storyIntelligence.admin.fields.chapterText
micro: Supply complete chapter prose with its paragraph and scene breaks.
quickTitle: Chapter Test Source Text
quickSummary: Supply complete chapter prose with its paragraph and scene breaks.
screenshot:
---
# Chapter Test Source Text
Paste the chapter you want to test rather than a summary. Preserve paragraph breaks and scene separators so the chapter structure can be evaluated. For the full chapter test, a supplied .txt upload takes precedence over pasted text.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Illustration Category Filter
category: Story Intelligence Admin
tags: story intelligence, admin, fields, illustrationCategory
contextKey: storyIntelligence.admin.fields.illustrationCategory
micro: Limit the library to the kind of illustration you want to inspect.
quickTitle: Illustration Category Filter
quickSummary: Limit the library to the kind of illustration you want to inspect.
screenshot:
---
# Illustration Category Filter
Choose a category to narrow the displayed library items, or use the all-categories choice. Apply Filter after changing the selection. Filtering does not change an illustration or its approval.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Illustration Metadata
category: Story Intelligence Admin
tags: story intelligence, admin, fields, illustrationMetadata
contextKey: storyIntelligence.admin.fields.illustrationMetadata
micro: Inspect the saved descriptive details attached to the library item.
quickTitle: Illustration Metadata
quickSummary: Inspect the saved descriptive details attached to the library item.
screenshot:
---
# Illustration Metadata
Use the read-only metadata to understand how the item is described and organised. It is not a place to edit story facts or character identities.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Illustration Admin Notes
category: Story Intelligence Admin
tags: story intelligence, admin, fields, illustrationNotes
contextKey: storyIntelligence.admin.fields.illustrationNotes
micro: Record review observations for this library item.
quickTitle: Illustration Admin Notes
quickSummary: Record review observations for this library item.
screenshot:
---
# Illustration Admin Notes
Note concrete issues such as an incorrect object, unsuitable style or why a regeneration is needed. Save notes to retain them. Notes do not themselves approve the image or change its generation prompt.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Illustration Generation Prompt
category: Story Intelligence Admin
tags: story intelligence, admin, fields, illustrationPrompt
contextKey: storyIntelligence.admin.fields.illustrationPrompt
micro: Read the instructions used to generate this library image.
quickTitle: Illustration Generation Prompt
quickSummary: Read the instructions used to generate this library image.
screenshot:
---
# Illustration Generation Prompt
Compare the prompt with the actual image when judging whether generation followed the intended brief. This displayed prompt is read-only and does not alter manuscript analysis.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Illustration Library Search
category: Story Intelligence Admin
tags: story intelligence, admin, fields, illustrationSearch
contextKey: storyIntelligence.admin.fields.illustrationSearch
micro: Find library items matching your search text.
quickTitle: Illustration Library Search
quickSummary: Find library items matching your search text.
screenshot:
---
# Illustration Library Search
Enter a recognisable term from the item you want to find, then apply Filter. Combine search with category and status when the library is large.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Illustration Specification
category: Story Intelligence Admin
tags: story intelligence, admin, fields, illustrationSpecification
contextKey: storyIntelligence.admin.fields.illustrationSpecification
micro: Inspect the intended content and constraints for the library item.
quickTitle: Illustration Specification
quickSummary: Inspect the intended content and constraints for the library item.
screenshot:
---
# Illustration Specification
Use the specification alongside the generated image when deciding whether it fits its intended library role. This is generation guidance, not evidence about an individual manuscript.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Illustration Status Filter
category: Story Intelligence Admin
tags: story intelligence, admin, fields, illustrationStatus
contextKey: storyIntelligence.admin.fields.illustrationStatus
micro: Find items at a particular generation or review stage.
quickTitle: Illustration Status Filter
quickSummary: Find items at a particular generation or review stage.
screenshot:
---
# Illustration Status Filter
Choose a status to inspect queued, failed, generated or reviewed work as offered in the list. Apply Filter to update the results. A generated image still needs an editorial check before approval.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Parsed Analysis Output
category: Story Intelligence Admin
tags: story intelligence, admin, fields, parsedOutput
contextKey: storyIntelligence.admin.fields.parsedOutput
micro: Inspect the structured result used for validation.
quickTitle: Parsed Analysis Output
quickSummary: Inspect the structured result used for validation.
screenshot:
---
# Parsed Analysis Output
This read-only JSON shows the parsed or normalised analysis result identified by the label. Compare missing or unexpected fields against the manuscript and validation messages. Editing story records happens elsewhere; these output boxes are diagnostic views.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Analysis Project Context
category: Story Intelligence Admin
tags: story intelligence, admin, fields, project
contextKey: storyIntelligence.admin.fields.project
micro: Use the project that owns the manuscript being tested.
quickTitle: Analysis Project Context
quickSummary: Use the project that owns the manuscript being tested.
screenshot:
---
# Analysis Project Context
Select the project or enter its saved ID as the form requires. IDs identify saved records, not their order in a list. Keep the book and chapter context consistent with the selected project; do not invent an ID to describe a new project.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Raw Analysis Response
category: Story Intelligence Admin
tags: story intelligence, admin, fields, rawOutput
contextKey: storyIntelligence.admin.fields.rawOutput
micro: Inspect the original response when diagnosing extraction or validation errors.
quickTitle: Raw Analysis Response
quickSummary: Inspect the original response when diagnosing extraction or validation errors.
screenshot:
---
# Raw Analysis Response
The label distinguishes extracted assistant output from the full response. Use these read-only values to understand a failed or incomplete analysis; they are not editable manuscript fields or accepted story facts.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Analysis Scene Context
category: Story Intelligence Admin
tags: story intelligence, admin, fields, scene
contextKey: storyIntelligence.admin.fields.scene
micro: Use the saved scene identity when testing an existing scene.
quickTitle: Analysis Scene Context
quickSummary: Use the saved scene identity when testing an existing scene.
screenshot:
---
# Analysis Scene Context
Enter the record ID for the scene being analysed when applicable. Do not substitute its display number. Ensure the supplied text and chapter context describe the same scene.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Scene Test Context Output
category: Story Intelligence Admin
tags: story intelligence, admin, fields, sceneContext
contextKey: storyIntelligence.admin.fields.sceneContext
micro: Inspect the context supplied for the scene test.
quickTitle: Scene Test Context Output
quickSummary: Inspect the context supplied for the scene test.
screenshot:
---
# Scene Test Context Output
This read-only JSON shows the scene context assembled for analysis. Check its character, setting and chronology information when a result appears to rely on the wrong context.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Scene Intelligence Test Model
category: Story Intelligence Admin
tags: story intelligence, admin, fields, sceneModel
contextKey: storyIntelligence.admin.fields.sceneModel
micro: Choose the model used to analyse the detected scenes in this test.
quickTitle: Scene Intelligence Test Model
quickSummary: Choose the model used to analyse the detected scenes in this test.
screenshot:
---
# Scene Intelligence Test Model
Use the available model list to compare extraction quality on the same source. This choice is separate from the chapter structure model. Different choices can affect completion time, cost estimates and results.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Analysis Scene Number
category: Story Intelligence Admin
tags: story intelligence, admin, fields, sceneNumber
contextKey: storyIntelligence.admin.fields.sceneNumber
micro: Give the scene position within the chapter.
quickTitle: Analysis Scene Number
quickSummary: Give the scene position within the chapter.
screenshot:
---
# Analysis Scene Number
Use the scene number that describes the supplied source within its chapter. This is contextual numbering and is distinct from the saved Scene ID.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Scene Test Source Text
category: Story Intelligence Admin
tags: story intelligence, admin, fields, sceneText
contextKey: storyIntelligence.admin.fields.sceneText
micro: Supply the actual scene prose to analyse.
quickTitle: Scene Test Source Text
quickSummary: Supply the actual scene prose to analyse.
screenshot:
---
# Scene Test Source Text
Paste one complete scene, preserving dialogue and paragraph breaks. Use manuscript text rather than a synopsis when testing evidence extraction. Check that the IDs and numbering, if supplied, describe the same source.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Analysis Source Label
category: Story Intelligence Admin
tags: story intelligence, admin, fields, sourceLabel
contextKey: storyIntelligence.admin.fields.sourceLabel
micro: Give this test a recognisable label for later comparison.
quickTitle: Analysis Source Label
quickSummary: Give this test a recognisable label for later comparison.
screenshot:
---
# Analysis Source Label
Use a short label such as Chapter 4 revision or Dialogue scene test. It helps identify the analysis attempt; it does not rename the manuscript chapter or change scene content.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Chapter Text Upload
category: Story Intelligence Admin
tags: story intelligence, admin, fields, upload
contextKey: storyIntelligence.admin.fields.upload
micro: Upload a plain-text chapter instead of pasting it.
quickTitle: Chapter Text Upload
quickSummary: Upload a plain-text chapter instead of pasting it.
screenshot:
---
# Chapter Text Upload
The full chapter test accepts .txt files up to 2 MB. A non-empty upload takes precedence over the chapter text box. Check the loaded filename and source counts before queueing so you analyse the intended version.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,31 +0,0 @@
---
title: Full Chapter Analysis Test
category: Story Intelligence Admin
tags: story intelligence, admin, fullChapterTest
contextKey: storyIntelligence.admin.fullChapterTest
micro: Check a large chapter and queue a saved background analysis run.
quickTitle: Full Chapter Analysis Test
quickSummary: Check a large chapter and queue a saved background analysis run.
screenshot:
---
# Full Chapter Analysis Test
Paste chapter text or upload a plain-text file, choose models, then analyse pre-flight. Inspect size estimates and warnings before queueing. A queued run saves analysis results for monitoring; it does not itself commit them as story records. Open the monitor to follow the run.
# Understanding the Fields
| Field | Suggested use |
| --- | --- |
| [Analysis Source Label](/Help/Article/storyIntelligence.admin.fields.sourceLabel) | Give this test a recognisable label for later comparison. |
| [Analysis Project Context](/Help/Article/storyIntelligence.admin.fields.project) | Use the project that owns the manuscript being tested. |
| [Analysis Book Context](/Help/Article/storyIntelligence.admin.fields.book) | Use the book that contains the chapter under analysis. |
| [Analysis Chapter Context](/Help/Article/storyIntelligence.admin.fields.chapter) | Use the saved chapter identity for the source text. |
| [Chapter Structure Test Model](/Help/Article/storyIntelligence.admin.fields.chapterModel) | Choose the model used to detect chapter structure in this test. |
| [Scene Intelligence Test Model](/Help/Article/storyIntelligence.admin.fields.sceneModel) | Choose the model used to analyse the detected scenes in this test. |
| [Chapter Text Upload](/Help/Article/storyIntelligence.admin.fields.upload) | Upload a plain-text chapter instead of pasting it. |
| [Chapter Test Source Text](/Help/Article/storyIntelligence.admin.fields.chapterText) | Supply complete chapter prose with its paragraph and scene breaks. |
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

View File

@ -1,18 +0,0 @@
---
title: Illustration Generation Queue
category: Story Intelligence Admin
tags: story intelligence, admin, illustrationQueue
contextKey: storyIntelligence.admin.illustrationQueue
micro: Follow library items from planning through generation and review.
quickTitle: Illustration Generation Queue
quickSummary: Follow library items from planning through generation and review.
screenshot:
---
# Illustration Generation Queue
Generate Starter Library prepares the starter set. Queue starts generation for an eligible item; Requeue All Failed retries failed work. Inspect generated images before approving them. Regeneration starts another generation attempt rather than editing your manuscript.
# Related Guides
- [Story Intelligence diagnostics](/Help/Article/storyIntelligence.admin.diagnostics)

Some files were not shown because too many files have changed in this diff Show More