Compare commits
No commits in common. "architecture/story-context-consolidation" and "onboarding" have entirely different histories.
architectu
...
onboarding
166
AGENTS.md
166
AGENTS.md
@ -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.
|
||||
@ -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.
|
||||
@ -6,8 +6,8 @@ The add-in is deliberately small:
|
||||
|
||||
- The manifest adds one ribbon button: **Open PlotDirector Companion**.
|
||||
- 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 companion only writes hidden PlotDirector anchors needed to reconnect manuscript paragraphs to PlotDirector scenes.
|
||||
- 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 does not request document write permissions.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@ -22,9 +22,9 @@
|
||||
<DefaultSettings>
|
||||
<SourceLocation DefaultValue="{{taskpaneUrl}}"/>
|
||||
</DefaultSettings>
|
||||
<!-- ReadWriteDocument is required because the companion writes hidden PlotDirector anchors
|
||||
back into the Word document after a manuscript import. -->
|
||||
<Permissions>ReadWriteDocument</Permissions>
|
||||
<!-- ReadDocument is required for the companion to read Word paragraphs, selection context,
|
||||
and document body content. It does not grant document write access. -->
|
||||
<Permissions>ReadDocument</Permissions>
|
||||
<VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
|
||||
<Hosts>
|
||||
<Host xsi:type="Document">
|
||||
|
||||
@ -22,9 +22,9 @@
|
||||
<DefaultSettings>
|
||||
<SourceLocation DefaultValue="https://plotdirector.com/word-companion"/>
|
||||
</DefaultSettings>
|
||||
<!-- ReadWriteDocument is required because the companion writes hidden PlotDirector anchors
|
||||
back into the Word document after a manuscript import. -->
|
||||
<Permissions>ReadWriteDocument</Permissions>
|
||||
<!-- ReadDocument is required for the companion to read Word paragraphs, selection context,
|
||||
and document body content. It does not grant document write access. -->
|
||||
<Permissions>ReadDocument</Permissions>
|
||||
<VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
|
||||
<Hosts>
|
||||
<Host xsi:type="Document">
|
||||
|
||||
@ -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.
|
||||
@ -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; });
|
||||
80
PlotLine.Tests/Browser/package-lock.json
generated
80
PlotLine.Tests/Browser/package-lock.json
generated
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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" }
|
||||
}
|
||||
@ -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; });
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
@ -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
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
@ -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>
|
||||
@ -1,7 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using PlotLine.Data;
|
||||
using PlotLine.Models;
|
||||
using PlotLine.Services;
|
||||
using PlotLine.ViewModels;
|
||||
@ -23,19 +21,8 @@ public sealed class AdminController(
|
||||
IStoryIntelligenceExistingChapterQueueService existingChapterQueue,
|
||||
IStoryIntelligenceFullChapterTestService fullChapterTest,
|
||||
IStoryIntelligenceImportCommitService storyIntelligenceImportCommit,
|
||||
INameIntelligenceService nameIntelligence,
|
||||
IIllustrationLibraryService illustrationLibrary,
|
||||
IProjectRepository projects,
|
||||
IBookRepository books,
|
||||
IOnboardingBuildRepository onboardingBuilds,
|
||||
ICoreImportBasicLocationService coreImportBasicLocations,
|
||||
IWebHostEnvironment environment,
|
||||
ILogger<AdminController> logger) : Controller
|
||||
IIllustrationLibraryService illustrationLibrary) : 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("index")]
|
||||
public async Task<IActionResult> Index()
|
||||
@ -52,138 +39,6 @@ public sealed class AdminController(
|
||||
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")]
|
||||
public async Task<IActionResult> StoryIntelligenceIllustrationLibrary([FromQuery] IllustrationLibraryFilter filter)
|
||||
{
|
||||
@ -314,27 +169,8 @@ public sealed class AdminController(
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
logger.LogWarning("Story Intelligence development reset requested by admin user {UserId}.", userId);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@ -69,8 +69,6 @@ public sealed class BooksController(IBookService books, IOnboardingService onboa
|
||||
createModel.BookNumber = model.BookNumber;
|
||||
createModel.Description = model.Description;
|
||||
createModel.TargetWordCount = model.TargetWordCount;
|
||||
createModel.StoryEra = model.StoryEra;
|
||||
createModel.PrimaryLocaleCode = model.PrimaryLocaleCode;
|
||||
createModel.WritingStartDate = model.WritingStartDate;
|
||||
createModel.TargetCompletionDate = model.TargetCompletionDate;
|
||||
createModel.PublicationDate = model.PublicationDate;
|
||||
|
||||
@ -51,7 +51,6 @@ public sealed class ChaptersController(IChapterService chapters) : Controller
|
||||
|
||||
createModel.ChapterNumber = model.ChapterNumber;
|
||||
createModel.ChapterTitle = model.ChapterTitle;
|
||||
createModel.POVCharacterID = model.POVCharacterID;
|
||||
createModel.Summary = model.Summary;
|
||||
createModel.RevisionStatusID = model.RevisionStatusID;
|
||||
return View("Edit", createModel);
|
||||
@ -60,15 +59,6 @@ public sealed class ChaptersController(IChapterService chapters) : Controller
|
||||
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]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Archive(int id, int bookId)
|
||||
|
||||
@ -32,12 +32,6 @@ public sealed class CharactersController(ICharacterService characters) : Control
|
||||
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)
|
||||
{
|
||||
var model = await characters.GetAvatarCropAsync(imageId);
|
||||
@ -105,93 +99,6 @@ public sealed class CharactersController(ICharacterService characters) : Control
|
||||
: 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)
|
||||
{
|
||||
if (model.SexValueID == CharacterEditViewModel.CustomSexValue)
|
||||
|
||||
193
PlotLine/Controllers/DevelopmentController.cs
Normal file
193
PlotLine/Controllers/DevelopmentController.cs
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -32,64 +32,6 @@ public sealed class LocationsController(ILocationService locations) : Controller
|
||||
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]
|
||||
[ValidateAntiForgeryToken]
|
||||
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 });
|
||||
}
|
||||
|
||||
[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]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Archive(int id, int projectId)
|
||||
@ -157,25 +82,13 @@ public sealed class LocationsController(ILocationService locations) : Controller
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> DropRelationship(LocationRelationshipEditViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid || 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
|
||||
if (model.FromLocationID != model.ToLocationID)
|
||||
{
|
||||
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 });
|
||||
}
|
||||
|
||||
private bool IsAjaxRequest() => Request.Headers["X-Requested-With"] == "XMLHttpRequest";
|
||||
return RedirectToAction(nameof(Index), new { projectId = model.ProjectID });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -3,19 +3,12 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using PlotLine.Models;
|
||||
using PlotLine.Services;
|
||||
using PlotLine.ViewModels;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace PlotLine.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[Route("onboarding")]
|
||||
public sealed class OnboardingController(
|
||||
IOnboardingService onboarding,
|
||||
IOnboardingStoryIntelligenceService storyIntelligence,
|
||||
IWholeBookPlotIntelligenceService wholeBookPlotIntelligence,
|
||||
ICharacterEnrichmentService characterEnrichment,
|
||||
ICurrentUserService currentUser,
|
||||
ILogger<OnboardingController> logger) : Controller
|
||||
public sealed class OnboardingController(IOnboardingService onboarding, IOnboardingStoryIntelligenceService storyIntelligence) : Controller
|
||||
{
|
||||
[HttpGet("")]
|
||||
public async Task<IActionResult> Index()
|
||||
@ -67,85 +60,11 @@ public sealed class OnboardingController(
|
||||
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["CharacterReviewPage"] = Math.Max(1, page);
|
||||
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")]
|
||||
public async Task<IActionResult> BuildComplete(Guid previewId)
|
||||
{
|
||||
@ -157,31 +76,18 @@ public sealed class OnboardingController(
|
||||
public async Task<IActionResult> StoryIntelligence()
|
||||
{
|
||||
var model = await storyIntelligence.GetOverviewAsync();
|
||||
if (model.ExistingBatchID.HasValue)
|
||||
{
|
||||
return RedirectToBook(model);
|
||||
}
|
||||
|
||||
if (model.PreviewID == Guid.Empty)
|
||||
{
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(BuildComplete), new { previewId = model.PreviewID });
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("story-intelligence/context")]
|
||||
public async Task<IActionResult> StoryIntelligenceContext()
|
||||
{
|
||||
return await StoryIntelligence();
|
||||
var model = await storyIntelligence.GetOverviewAsync();
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("story-intelligence/book/{bookId:int}/continue")]
|
||||
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);
|
||||
if (target is null)
|
||||
@ -192,117 +98,32 @@ public sealed class OnboardingController(
|
||||
|
||||
return target.Route switch
|
||||
{
|
||||
StoryIntelligenceResumeRoutes.ReviewCentre => await StoryIntelligenceReviewCentre(target.BatchID),
|
||||
StoryIntelligenceResumeRoutes.Complete => await StoryIntelligenceReviewCentre(target.BatchID),
|
||||
StoryIntelligenceResumeRoutes.Narrative => await StoryIntelligenceReviewCentre(target.BatchID),
|
||||
StoryIntelligenceResumeRoutes.Knowledge => await StoryIntelligenceReviewCentre(target.BatchID),
|
||||
StoryIntelligenceResumeRoutes.Relationships => await StoryIntelligenceReviewCentre(target.BatchID),
|
||||
StoryIntelligenceResumeRoutes.Assets => await StoryIntelligenceReviewCentre(target.BatchID),
|
||||
StoryIntelligenceResumeRoutes.Locations => await StoryIntelligenceReviewCentre(target.BatchID),
|
||||
StoryIntelligenceResumeRoutes.Characters => await StoryIntelligenceReviewCentre(target.BatchID),
|
||||
_ => RedirectToAction("Details", "Books", new { id = bookId })
|
||||
StoryIntelligenceResumeRoutes.Complete => RedirectToAction(nameof(StoryIntelligenceComplete), new { batchId = target.BatchID }),
|
||||
StoryIntelligenceResumeRoutes.Knowledge => RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = target.BatchID }),
|
||||
StoryIntelligenceResumeRoutes.Relationships => RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId = target.BatchID }),
|
||||
StoryIntelligenceResumeRoutes.Assets => RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId = target.BatchID }),
|
||||
StoryIntelligenceResumeRoutes.Locations => RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = target.BatchID }),
|
||||
StoryIntelligenceResumeRoutes.Characters => RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId = target.BatchID }),
|
||||
_ => RedirectToAction(nameof(StoryIntelligenceReview), new { batchId = target.BatchID })
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("story-intelligence/start")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> StartStoryIntelligence()
|
||||
=> RedirectToAction(nameof(StoryIntelligencePlotContext));
|
||||
|
||||
[HttpGet("story-intelligence/plot-context")]
|
||||
public async Task<IActionResult> StoryIntelligencePlotContext()
|
||||
public async Task<IActionResult> StartStoryIntelligence(
|
||||
[FromForm] string? appearancePreference,
|
||||
[FromForm] string? primaryAgeGroup,
|
||||
[FromForm] string? storyEra,
|
||||
[FromForm] string? storyLocation)
|
||||
{
|
||||
var overview = await storyIntelligence.GetOverviewAsync();
|
||||
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 });
|
||||
}
|
||||
|
||||
var job = await storyIntelligence.StartAsync(appearancePreference, primaryAgeGroup, storyEra, storyLocation);
|
||||
if (job is null)
|
||||
{
|
||||
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);
|
||||
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 });
|
||||
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId = job.BatchID });
|
||||
}
|
||||
|
||||
[HttpPost("story-intelligence/skip")]
|
||||
@ -326,14 +147,7 @@ public sealed class OnboardingController(
|
||||
public async Task<IActionResult> StoryIntelligenceProgress(Guid batchId)
|
||||
{
|
||||
var model = await storyIntelligence.GetProgressAsync(batchId);
|
||||
if (model is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return model.SceneAnalysisComplete
|
||||
? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId })
|
||||
: RedirectToBook(model);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
|
||||
[HttpGet("story-intelligence/review")]
|
||||
@ -347,50 +161,18 @@ public sealed class OnboardingController(
|
||||
|
||||
if (model.HasActiveRuns)
|
||||
{
|
||||
return RedirectToBook(model);
|
||||
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
|
||||
}
|
||||
|
||||
return model.SceneAnalysisComplete
|
||||
? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId })
|
||||
: RedirectToBook(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);
|
||||
return model.AllCommitted
|
||||
? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId })
|
||||
: View(model);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
return NotFound();
|
||||
@ -398,21 +180,18 @@ public sealed class OnboardingController(
|
||||
|
||||
if (model.HasActiveRuns)
|
||||
{
|
||||
return RedirectToBook(model);
|
||||
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
|
||||
}
|
||||
|
||||
if (!model.SceneAnalysisComplete)
|
||||
{
|
||||
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
|
||||
}
|
||||
|
||||
return View(model);
|
||||
return !model.AllCommitted
|
||||
? RedirectToAction(nameof(StoryIntelligenceReview), new { batchId })
|
||||
: View(model);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
return NotFound();
|
||||
@ -420,23 +199,23 @@ public sealed class OnboardingController(
|
||||
|
||||
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
|
||||
? RedirectToReviewCentreWithCharacterGate(batchId)
|
||||
return !model.PipelineDashboard.CharacterStageComplete
|
||||
? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId })
|
||||
: View(model);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
return NotFound();
|
||||
@ -444,26 +223,28 @@ public sealed class OnboardingController(
|
||||
|
||||
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")]
|
||||
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)
|
||||
{
|
||||
return NotFound();
|
||||
@ -471,20 +252,27 @@ public sealed class OnboardingController(
|
||||
|
||||
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")]
|
||||
@ -498,7 +286,7 @@ public sealed class OnboardingController(
|
||||
}
|
||||
|
||||
TempData["OnboardingStoryIntelligenceMessage"] = "Analysis cancellation requested.";
|
||||
return RedirectToBook(progress);
|
||||
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
||||
}
|
||||
|
||||
[HttpPost("story-intelligence/commit")]
|
||||
@ -512,7 +300,13 @@ public sealed class OnboardingController(
|
||||
}
|
||||
|
||||
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")]
|
||||
@ -526,7 +320,9 @@ public sealed class OnboardingController(
|
||||
}
|
||||
|
||||
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")]
|
||||
@ -541,13 +337,20 @@ public sealed class OnboardingController(
|
||||
|
||||
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
||||
return result.Success
|
||||
? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID })
|
||||
? RedirectToAction(nameof(StoryIntelligenceCharacterComplete), 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")]
|
||||
[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);
|
||||
if (progress is null)
|
||||
@ -556,31 +359,21 @@ public sealed class OnboardingController(
|
||||
}
|
||||
|
||||
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
||||
if (!result.Success)
|
||||
{
|
||||
return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = form.BatchID, page = Math.Max(1, form.PageNumber), scope = form.Scope });
|
||||
return result.Success
|
||||
? RedirectToAction(nameof(StoryIntelligenceLocationComplete), new { batchId = form.BatchID })
|
||||
: RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId = form.BatchID });
|
||||
}
|
||||
|
||||
var pendingCount = progress.LocationReview.PendingCandidateCount;
|
||||
if (pendingCount <= 0)
|
||||
[HttpGet("story-intelligence/locations/complete")]
|
||||
public async Task<IActionResult> StoryIntelligenceLocationComplete(Guid batchId)
|
||||
{
|
||||
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(StoryIntelligenceLocations), new { batchId = form.BatchID, page = targetPage, scope = form.Scope });
|
||||
var model = await storyIntelligence.GetLocationImportResultAsync(batchId);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
|
||||
[HttpPost("story-intelligence/assets")]
|
||||
[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);
|
||||
if (progress is null)
|
||||
@ -589,37 +382,22 @@ public sealed class OnboardingController(
|
||||
}
|
||||
|
||||
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
||||
if (!result.Success)
|
||||
{
|
||||
return RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId = form.BatchID, page = Math.Max(1, form.PageNumber) });
|
||||
return result.Success
|
||||
? RedirectToAction(nameof(StoryIntelligenceAssetComplete), new { batchId = form.BatchID })
|
||||
: RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId = form.BatchID });
|
||||
}
|
||||
|
||||
var pendingCount = progress.AssetReview.SummaryNeedsAttentionCount ?? progress.AssetReview.PendingCandidateCount;
|
||||
if (pendingCount <= 0)
|
||||
[HttpGet("story-intelligence/assets/complete")]
|
||||
public async Task<IActionResult> StoryIntelligenceAssetComplete(Guid batchId)
|
||||
{
|
||||
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(StoryIntelligenceAssets), new { batchId = form.BatchID, page = targetPage });
|
||||
var model = await storyIntelligence.GetAssetImportResultAsync(batchId);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
|
||||
[HttpPost("story-intelligence/relationships")]
|
||||
[ValidateAntiForgeryToken]
|
||||
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);
|
||||
if (progress is null)
|
||||
{
|
||||
@ -628,14 +406,21 @@ public sealed class OnboardingController(
|
||||
|
||||
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
||||
return result.Success
|
||||
? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID })
|
||||
? RedirectToAction(nameof(StoryIntelligenceRelationshipComplete), new { batchId = form.BatchID })
|
||||
: RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId = form.BatchID });
|
||||
}
|
||||
|
||||
[HttpGet("story-intelligence/knowledge")]
|
||||
public async Task<IActionResult> StoryIntelligenceKnowledge(Guid batchId, int page = 1)
|
||||
[HttpGet("story-intelligence/relationships/complete")]
|
||||
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)
|
||||
{
|
||||
return NotFound();
|
||||
@ -643,25 +428,37 @@ public sealed class OnboardingController(
|
||||
|
||||
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")]
|
||||
[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);
|
||||
if (progress is null)
|
||||
@ -670,119 +467,54 @@ public sealed class OnboardingController(
|
||||
}
|
||||
|
||||
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
||||
if (!result.Success)
|
||||
{
|
||||
return RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = form.BatchID, page = Math.Max(1, form.PageNumber) });
|
||||
return result.Success
|
||||
? RedirectToAction(nameof(StoryIntelligenceKnowledgeComplete), new { batchId = form.BatchID })
|
||||
: RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId = form.BatchID });
|
||||
}
|
||||
|
||||
var pendingCount = progress.KnowledgeReview.SummaryNeedsAttentionCount ?? progress.KnowledgeReview.PendingCandidateCount;
|
||||
if (pendingCount <= 0)
|
||||
[HttpGet("story-intelligence/knowledge/complete")]
|
||||
public async Task<IActionResult> StoryIntelligenceKnowledgeComplete(Guid batchId)
|
||||
{
|
||||
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID });
|
||||
var model = await storyIntelligence.GetKnowledgeImportResultAsync(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(StoryIntelligenceKnowledge), new { batchId = form.BatchID, page = targetPage });
|
||||
}
|
||||
|
||||
[HttpGet("story-intelligence/narrative")]
|
||||
public async Task<IActionResult> StoryIntelligenceNarrative(Guid batchId)
|
||||
[HttpGet("story-intelligence/complete")]
|
||||
public async Task<IActionResult> StoryIntelligenceComplete(Guid batchId)
|
||||
{
|
||||
var model = await storyIntelligence.GetNarrativeReviewAsync(batchId);
|
||||
if (model is null)
|
||||
{
|
||||
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")]
|
||||
[ValidateAntiForgeryToken]
|
||||
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)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
||||
return result.Success
|
||||
? RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId = form.BatchID })
|
||||
: RedirectToAction(nameof(StoryIntelligenceNarrative), new { batchId = form.BatchID });
|
||||
if (!progress.PipelineDashboard.CharacterStageComplete)
|
||||
{
|
||||
return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
|
||||
}
|
||||
|
||||
[HttpPost("story-intelligence/plot-synthesis/retry")]
|
||||
public async Task<IActionResult> RetryStoryIntelligencePlotSynthesis(Guid batchId)
|
||||
if (!progress.PipelineDashboard.LocationStageComplete)
|
||||
{
|
||||
var model = await storyIntelligence.GetReviewCentreAsync(batchId);
|
||||
if (model is null)
|
||||
{
|
||||
return NotFound();
|
||||
return RedirectToAction(nameof(StoryIntelligenceLocations), new { batchId });
|
||||
}
|
||||
|
||||
if (!currentUser.UserId.HasValue)
|
||||
if (!progress.PipelineDashboard.AssetStageComplete)
|
||||
{
|
||||
return Forbid();
|
||||
return RedirectToAction(nameof(StoryIntelligenceAssets), new { batchId });
|
||||
}
|
||||
|
||||
await wholeBookPlotIntelligence.RetryAsync(model.BookID, currentUser.UserId.Value);
|
||||
TempData["OnboardingStoryIntelligenceMessage"] = "Plot Lines analysis has been queued again.";
|
||||
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
|
||||
if (!progress.PipelineDashboard.RelationshipStageComplete)
|
||||
{
|
||||
return RedirectToAction(nameof(StoryIntelligenceRelationships), new { batchId });
|
||||
}
|
||||
|
||||
[HttpPost("story-intelligence/character-enrichment/retry")]
|
||||
public async Task<IActionResult> RetryStoryIntelligenceCharacterEnrichment(Guid batchId)
|
||||
if (!progress.PipelineDashboard.KnowledgeStageComplete)
|
||||
{
|
||||
var model = await storyIntelligence.GetReviewCentreAsync(batchId);
|
||||
if (model is null)
|
||||
{
|
||||
return NotFound();
|
||||
return RedirectToAction(nameof(StoryIntelligenceKnowledge), new { batchId });
|
||||
}
|
||||
|
||||
if (!currentUser.UserId.HasValue)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
await characterEnrichment.RetryAsync(model.BookID, currentUser.UserId.Value);
|
||||
TempData["OnboardingStoryIntelligenceMessage"] = "Character enrichment has been queued again.";
|
||||
return RedirectToAction(nameof(StoryIntelligenceReviewCentre), new { batchId });
|
||||
}
|
||||
|
||||
private IActionResult RedirectToBook(StoryIntelligenceProgressViewModel model)
|
||||
=> RedirectToAction("Details", "Books", new { id = model.BookID });
|
||||
|
||||
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 });
|
||||
var model = await storyIntelligence.GetCompletionAsync(batchId);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
|
||||
[HttpPost("scan-review")]
|
||||
@ -794,54 +526,25 @@ public sealed class OnboardingController(
|
||||
var continueToCharacters = string.Equals(intent, "characters", StringComparison.OrdinalIgnoreCase);
|
||||
var continueCharacterReview = string.Equals(intent, "character-next", StringComparison.OrdinalIgnoreCase);
|
||||
var backCharacterReview = string.Equals(intent, "character-back", StringComparison.OrdinalIgnoreCase);
|
||||
var readyToImport = string.Equals(intent, "structure", StringComparison.OrdinalIgnoreCase);
|
||||
var completeCoreImport = string.Equals(intent, "continue", StringComparison.OrdinalIgnoreCase);
|
||||
var readyToImport = string.Equals(intent, "continue", StringComparison.OrdinalIgnoreCase);
|
||||
var nextCharacterPage = Math.Max(1, characterPage + (backCharacterReview ? -1 : 1));
|
||||
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);
|
||||
if (model is null)
|
||||
{
|
||||
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
|
||||
? "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."
|
||||
: continueCharacterReview || backCharacterReview ? "Character review choices saved."
|
||||
: "Review choices saved.";
|
||||
return readyToImport
|
||||
? RedirectToAction(nameof(Index))
|
||||
? RedirectToAction(nameof(StoryIntelligence))
|
||||
: 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 });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
@ -853,7 +556,7 @@ public sealed class OnboardingController(
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (continueToCharacters || continueCharacterReview || backCharacterReview || completeCoreImport)
|
||||
if (readyToImport || continueCharacterReview || backCharacterReview)
|
||||
{
|
||||
ViewData["ScanReviewStep"] = "characters";
|
||||
ViewData["CharacterReviewPage"] = readyToImport
|
||||
|
||||
@ -50,10 +50,8 @@ public sealed class ProjectMetricsController(ISceneMetricTypeService metrics) :
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> AddMissingDefaults(int projectId)
|
||||
{
|
||||
var backfilled = await metrics.AddMissingDefaultsAsync(projectId);
|
||||
TempData["ProjectMetricMessage"] = backfilled > 0
|
||||
? $"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.";
|
||||
await metrics.AddMissingDefaultsAsync(projectId);
|
||||
TempData["ProjectMetricMessage"] = "Missing default scene metrics added.";
|
||||
return RedirectToAction(nameof(Index), new { projectId });
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ using PlotLine.ViewModels;
|
||||
namespace PlotLine.Controllers;
|
||||
|
||||
[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());
|
||||
|
||||
@ -92,19 +92,15 @@ public sealed class ProjectsController(IProjectService projects, IProjectActivit
|
||||
[ValidateAntiForgeryToken]
|
||||
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);
|
||||
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.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
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.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
@ -112,12 +108,10 @@ public sealed class ProjectsController(IProjectService projects, IProjectActivit
|
||||
var result = await projects.HardDeleteAsync(projectId);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
logger.LogWarning("Permanent project delete completed for archived project {ProjectID}.", projectId);
|
||||
TempData["ArchiveMessage"] = "Project permanently deleted.";
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning("Permanent project delete failed for archived project {ProjectID}: {Message}", projectId, result.Message);
|
||||
TempData["ArchiveError"] = result.NotArchived
|
||||
? "Only archived projects can be permanently deleted."
|
||||
: result.Message;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using PlotLine.Models;
|
||||
using PlotLine.Services;
|
||||
|
||||
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)
|
||||
{
|
||||
var position = StoryPosition.FromTimelineSelection(projectId, bookId, chapterId, sceneId);
|
||||
var model = position is null
|
||||
? null
|
||||
: await relationshipMap.GetAsync(position, focusCharacterId, showFullNetwork);
|
||||
var model = await relationshipMap.GetAsync(projectId, bookId, chapterId, sceneId, focusCharacterId, showFullNetwork);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,36 +32,6 @@ public sealed class StoryAssetsController(IAssetService assets) : Controller
|
||||
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]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Save(StoryAssetEditViewModel model)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -13,9 +13,7 @@ namespace PlotLine.Controllers;
|
||||
[Route("api/word-companion")]
|
||||
public sealed class WordCompanionController(
|
||||
IWordCompanionService wordCompanion,
|
||||
IWordCompanionPresenceService presence,
|
||||
IHubContext<WordCompanionFollowHub> followHub,
|
||||
ILogger<WordCompanionController> logger) : ControllerBase
|
||||
IHubContext<WordCompanionFollowHub> followHub) : ControllerBase
|
||||
{
|
||||
[HttpGet("projects")]
|
||||
public Task<WordCompanionProjectsResponse> GetProjects() => wordCompanion.ListProjectsAsync();
|
||||
@ -126,54 +124,19 @@ public sealed class WordCompanionController(
|
||||
[HttpPost("runtime/current-scene")]
|
||||
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);
|
||||
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();
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
response.ProjectId,
|
||||
response.BookId,
|
||||
response.ChapterId,
|
||||
response.SceneId,
|
||||
response.SceneUrl,
|
||||
response.Position
|
||||
});
|
||||
|
||||
await BroadcastToCurrentUserAsync("storyPositionChanged", new
|
||||
{
|
||||
Source = "WordCompanion",
|
||||
response.ProjectId,
|
||||
response.BookId,
|
||||
response.ChapterId,
|
||||
response.SceneId,
|
||||
CanonicalUrl = response.SceneUrl,
|
||||
response.Position
|
||||
response.SceneUrl
|
||||
});
|
||||
|
||||
return Ok(response);
|
||||
@ -292,12 +255,4 @@ public sealed class WordCompanionController(
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,21 +7,13 @@ namespace PlotLine.Controllers;
|
||||
|
||||
[AllowAnonymous]
|
||||
[Route("word-companion")]
|
||||
public sealed class WordCompanionHostController(
|
||||
ICurrentUserService currentUser,
|
||||
IAuthorizationService authorization,
|
||||
ILogger<WordCompanionHostController> logger) : Controller
|
||||
public sealed class WordCompanionHostController(ICurrentUserService currentUser, IAuthorizationService authorization) : Controller
|
||||
{
|
||||
[HttpGet("")]
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var isAdmin = currentUser.IsAuthenticated
|
||||
&& (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
|
||||
{
|
||||
@ -33,3 +25,4 @@ public sealed class WordCompanionHostController(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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" : "")}.";
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -107,9 +107,6 @@ public sealed class IllustrationLibraryRepository(ISqlConnectionFactory connecti
|
||||
image.MimeType,
|
||||
Provider = providerResult.Provider,
|
||||
Model = providerResult.Model,
|
||||
providerResult.Quality,
|
||||
providerResult.Size,
|
||||
providerResult.OutputFormat,
|
||||
PromptText = prompt,
|
||||
PromptTemplateVersion = templateVersion,
|
||||
ProviderRequestID = providerResult.ProviderRequestID
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -8,16 +8,6 @@ namespace PlotLine.Data;
|
||||
public interface IOnboardingBuildRepository
|
||||
{
|
||||
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
|
||||
@ -41,22 +31,6 @@ public sealed class OnboardingBuildRepository(ISqlConnectionFactory connectionFa
|
||||
},
|
||||
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>();
|
||||
if (header is null)
|
||||
{
|
||||
@ -87,115 +61,6 @@ public sealed class OnboardingBuildRepository(ISqlConnectionFactory connectionFa
|
||||
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
|
||||
@ -222,7 +87,6 @@ public sealed class OnboardingBuildSceneRequest
|
||||
public string TemporarySceneKey { get; init; } = string.Empty;
|
||||
public string TemporaryChapterKey { get; init; } = string.Empty;
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string? SuggestedTitle { get; init; }
|
||||
public int SortOrder { get; init; }
|
||||
public int WordCount { get; init; }
|
||||
}
|
||||
@ -233,54 +97,3 @@ public sealed class OnboardingBuildCharacterRequest
|
||||
public string Name { get; init; } = string.Empty;
|
||||
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; }
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
@ -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"
|
||||
}
|
||||
@ -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,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
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,15 +0,0 @@
|
||||
{
|
||||
"ok": true,
|
||||
"counts": {
|
||||
"locales": 6,
|
||||
"givenNames": 3067,
|
||||
"relationships": 525,
|
||||
"usages": 92010
|
||||
},
|
||||
"coverage": {
|
||||
"usageGroups": 18402,
|
||||
"expectedUsageGroups": 18402
|
||||
},
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
}
|
||||
@ -75,8 +75,6 @@ public interface IChapterRepository
|
||||
Task<IReadOnlyList<Chapter>> ListByBookAsync(int bookId);
|
||||
Task<Chapter?> GetAsync(int chapterId);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -86,7 +84,7 @@ public interface ISceneRepository
|
||||
Task<IReadOnlyList<Scene>> ListByFloorPlanAsync(int floorPlanId);
|
||||
Task<Scene?> GetAsync(int sceneId);
|
||||
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 SavePurposesAsync(int sceneId, IEnumerable<int> purposeIds);
|
||||
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> MoveAsync(int metricTypeId, int projectId, string direction);
|
||||
Task AddMissingDefaultsAsync(int projectId);
|
||||
Task<int> BackfillDefaultValuesForProjectAsync(int projectId);
|
||||
Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId);
|
||||
Task<bool> HasProjectMetricsAsync(int projectId);
|
||||
}
|
||||
@ -168,17 +165,8 @@ public interface IPlotRepository
|
||||
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
|
||||
{
|
||||
Task<EntityMergeResult?> MergeAsync(int sourceId, int targetId, int? userId);
|
||||
Task<AssetLookupData> GetLookupsAsync(int? projectId = null);
|
||||
Task<IReadOnlyList<StoryAsset>> ListAssetsAsync(int projectId);
|
||||
Task<StoryAsset?> GetAssetAsync(int storyAssetId);
|
||||
@ -219,8 +207,6 @@ public interface ICharacterRepository
|
||||
Task<int> AddAliasAsync(int characterId, string alias, int? sortOrder);
|
||||
Task UpdateAliasAsync(int characterAliasId, string alias, int? sortOrder);
|
||||
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<CharacterSexValue?> GetSexValueAsync(int characterSexValueId, int ownerUserId);
|
||||
Task<int> GetOrCreateSexValueAsync(int ownerUserId, string sexName);
|
||||
@ -250,7 +236,6 @@ public interface ICharacterRepository
|
||||
Task<IReadOnlyList<CharacterRelationship>> ListInitialRelationshipsByCharacterAsync(int characterId);
|
||||
Task<CharacterRelationship?> GetRelationshipAsync(int characterRelationshipId);
|
||||
Task<int> SaveRelationshipAsync(CharacterRelationship relationship);
|
||||
Task<int> AcceptRelationshipDevelopmentAsync(RelationshipDevelopmentAcceptance proposal);
|
||||
Task ArchiveRelationshipAsync(int characterRelationshipId);
|
||||
Task<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsBySceneAsync(int sceneId);
|
||||
Task<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsByCharacterAsync(int characterId);
|
||||
@ -262,18 +247,14 @@ public interface ICharacterRepository
|
||||
|
||||
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<IReadOnlyList<LocationItem>> ListByProjectAsync(int projectId);
|
||||
Task<LocationItem?> GetAsync(int locationId);
|
||||
Task<int> SaveAsync(LocationItem location);
|
||||
Task<IReadOnlyList<LocationAlias>> ListAliasesAsync(int locationId);
|
||||
Task<IReadOnlyList<LocationAlias>> ListAliasesByProjectAsync(int projectId);
|
||||
Task<int> AddAliasAsync(int locationId, string alias, int? sortOrder);
|
||||
Task UpdateAliasAsync(int locationAliasId, string alias, int? sortOrder);
|
||||
Task DeleteAliasAsync(int locationAliasId);
|
||||
Task SetPreferredNameAsync(int locationId, string preferredName);
|
||||
Task ArchiveAsync(int locationId);
|
||||
Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByProjectAsync(int projectId);
|
||||
Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByLocationAsync(int locationId);
|
||||
@ -490,37 +471,6 @@ public sealed class TimelineSceneOverviewRow
|
||||
|
||||
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()
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
@ -575,16 +525,6 @@ public sealed class LocationRepository(ISqlConnectionFactory connectionFactory)
|
||||
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)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
@ -612,15 +552,6 @@ public sealed class LocationRepository(ISqlConnectionFactory connectionFactory)
|
||||
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)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
@ -2166,31 +2097,10 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) :
|
||||
public async Task HardDeleteForOwnerAsync(int projectId, int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
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)
|
||||
@ -2206,43 +2116,10 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) :
|
||||
public async Task HardDeleteForAccountDeletionAsync(int projectId, int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
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.Description,
|
||||
book.TargetWordCount,
|
||||
book.StoryEra,
|
||||
book.PrimaryLocaleCode,
|
||||
book.WritingStartDate,
|
||||
book.TargetCompletionDate,
|
||||
book.PublicationDate,
|
||||
@ -2522,33 +2397,7 @@ public sealed class ChapterRepository(ISqlConnectionFactory connectionFactory) :
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
"dbo.Chapter_Save",
|
||||
new { chapter.ChapterID, chapter.BookID, chapter.ChapterNumber, chapter.ChapterTitle, chapter.Summary, chapter.ChapterPurposeID, chapter.RevisionStatusID, chapter.POVCharacterID },
|
||||
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 },
|
||||
new { chapter.ChapterID, chapter.BookID, chapter.ChapterNumber, chapter.ChapterTitle, chapter.Summary, chapter.ChapterPurposeID, chapter.RevisionStatusID },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
@ -2621,27 +2470,17 @@ public sealed class SceneRepository(ISqlConnectionFactory connectionFactory) : I
|
||||
scene.RevisionStatusID,
|
||||
scene.PrimaryLocationID,
|
||||
scene.FloorPlanID,
|
||||
scene.InitialFloorPlanFloorID,
|
||||
scene.POVCharacterID
|
||||
scene.InitialFloorPlanFloorID
|
||||
},
|
||||
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();
|
||||
await connection.ExecuteAsync(
|
||||
"dbo.Scene_UpdatePovCharacter",
|
||||
new
|
||||
{
|
||||
SceneID = sceneId,
|
||||
POVCharacterID = povCharacterId,
|
||||
POVSource = source,
|
||||
POVConfidence = confidence,
|
||||
POVEvidence = evidence,
|
||||
POVSuggestedName = suggestedName,
|
||||
OverwriteAuthor = overwriteAuthor
|
||||
},
|
||||
new { SceneID = sceneId, POVCharacterID = povCharacterId },
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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 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)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
@ -3826,31 +3542,6 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
|
||||
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)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
@ -3980,12 +3671,7 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
|
||||
sceneCharacter.OutfitDescription,
|
||||
sceneCharacter.PhysicalCondition,
|
||||
sceneCharacter.EmotionalState,
|
||||
sceneCharacter.KnowledgeNotes,
|
||||
sceneCharacter.MatchSource,
|
||||
sceneCharacter.MatchedText,
|
||||
sceneCharacter.OccurrenceCount,
|
||||
sceneCharacter.FirstOccurrenceIndex,
|
||||
PreserveExistingWhenNull = string.Equals(sceneCharacter.MatchSource, "CoreImportDeterministic", StringComparison.Ordinal)
|
||||
sceneCharacter.KnowledgeNotes
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
@ -4127,13 +3813,6 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
|
||||
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)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
|
||||
117
PlotLine/Data/StoryIntelligenceIllustrationMatchingRepository.cs
Normal file
117
PlotLine/Data/StoryIntelligenceIllustrationMatchingRepository.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -9,13 +9,9 @@ public interface IStoryIntelligencePipelineRepository
|
||||
Task<StoryIntelligenceBookPipelineState?> GetByIdForUserAsync(int importSessionId, int userId);
|
||||
Task<StoryIntelligenceBookPipelineState?> GetByBookAsync(int bookId);
|
||||
Task<StoryIntelligenceBookPipelineState?> GetByBookForUserAsync(int bookId, int userId);
|
||||
Task<StoryIntelligenceReviewCentreSummary?> GetReviewCentreSummaryAsync(int bookId, int userId);
|
||||
Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId);
|
||||
Task<StoryIntelligenceBookPipelineState> UpsertAsync(StoryIntelligenceBookPipelineSaveRequest request);
|
||||
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
|
||||
@ -47,15 +43,6 @@ public sealed class StoryIntelligencePipelineRepository(ISqlConnectionFactory co
|
||||
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)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
@ -80,13 +67,7 @@ public sealed class StoryIntelligencePipelineRepository(ISqlConnectionFactory co
|
||||
request.CurrentReviewStage,
|
||||
request.Status,
|
||||
request.CompletedUtc,
|
||||
request.LastRunID,
|
||||
request.MarkCharacterReviewComplete,
|
||||
request.MarkLocationReviewComplete,
|
||||
request.MarkAssetReviewComplete,
|
||||
request.MarkRelationshipReviewComplete,
|
||||
request.MarkKnowledgeReviewComplete,
|
||||
request.MarkNarrativeReviewComplete
|
||||
request.LastRunID
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
@ -100,30 +81,4 @@ public sealed class StoryIntelligencePipelineRepository(ISqlConnectionFactory co
|
||||
commandType: CommandType.StoredProcedure);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@ public interface IStoryIntelligenceResultRepository
|
||||
Task<int> SaveAsync(StoryIntelligenceRunSaveRequest request);
|
||||
Task<int> QueueAdminTextAsync(StoryIntelligenceRunQueueRequest request);
|
||||
Task<StoryIntelligenceQueuedRun?> ClaimNextPendingAsync();
|
||||
Task<StoryIntelligenceQueuedRun?> ClaimNextPendingFairAsync(int maxConcurrentRunsPerBook, int maxConcurrentRunsPerUser, int claimLeaseMinutes);
|
||||
Task UpdateProgressAsync(
|
||||
int runId,
|
||||
string? status = null,
|
||||
@ -32,11 +31,9 @@ public interface IStoryIntelligenceResultRepository
|
||||
Task<int> SaveSceneResultAsync(int runId, int? chapterResultId, StoryIntelligenceSceneResultSaveRequest scene);
|
||||
Task<IReadOnlyList<StoryIntelligenceSavedRunListItem>> ListRunsAsync();
|
||||
Task<IReadOnlyList<StoryIntelligenceSavedRun>> ListRunsByBookForUserAsync(int bookId, int userId);
|
||||
Task<IReadOnlyList<StoryIntelligenceActiveBookRunSummary>> ListActiveBookRunSummariesForUserAsync(int userId);
|
||||
Task<StoryIntelligenceSavedRun?> GetRunAsync(int runId);
|
||||
Task<StoryIntelligenceSavedChapterResult?> GetChapterResultAsync(int runId);
|
||||
Task<IReadOnlyList<StoryIntelligenceSavedSceneResult>> ListSceneResultsAsync(int runId);
|
||||
Task<IReadOnlyList<Scene>> ListChapterScenesAsync(int chapterId);
|
||||
Task<StoryIntelligenceImportCommit?> GetImportCommitAsync(int runId);
|
||||
Task<StoryIntelligenceChapterImportReadiness> GetChapterImportReadinessAsync(int chapterId);
|
||||
Task<StoryIntelligenceImportCommitResult> CommitImportAsync(StoryIntelligenceImportCommitRequest request);
|
||||
@ -323,23 +320,6 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
||||
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(
|
||||
int runId,
|
||||
string? status = null,
|
||||
@ -509,16 +489,6 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
||||
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)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
@ -547,20 +517,6 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
||||
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)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
@ -610,31 +566,15 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
||||
new { request.ChapterID },
|
||||
transaction,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
var existingScenes = new List<Scene>();
|
||||
var orderedScenes = request.Scenes.OrderBy(scene => scene.TemporarySceneNumber).ToList();
|
||||
if (!readiness.CanCommit)
|
||||
{
|
||||
existingScenes = (await connection.QueryAsync<Scene>(
|
||||
"dbo.Scene_ListByChapter",
|
||||
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();
|
||||
return new StoryIntelligenceImportCommitResult
|
||||
{
|
||||
Success = false,
|
||||
Message = $"This chapter contains {existingScenes.Count:N0} existing scene(s), but Story Intelligence produced {orderedScenes.Count:N0} scene result(s)."
|
||||
Message = readiness.BlockReason ?? "This chapter already contains scenes."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.ChapterSummary) || request.ChapterPurposeID.HasValue)
|
||||
{
|
||||
@ -650,22 +590,17 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
var orderedScenes = request.Scenes.OrderBy(scene => scene.TemporarySceneNumber).ToList();
|
||||
for (var index = 0; index < orderedScenes.Count; 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";
|
||||
|
||||
try
|
||||
{
|
||||
operation = "saving scene";
|
||||
var relativeTimeText = TrimOptionalText(
|
||||
preserveExistingChronology
|
||||
? existingScene?.RelativeTimeText
|
||||
: item.RelativeTimeText ?? item.ParsedScene.Setting?.DateOrTimeReference ?? item.ParsedScene.Setting?.TimeOfDay,
|
||||
item.ParsedScene.Setting?.DateOrTimeReference ?? item.ParsedScene.Setting?.TimeOfDay,
|
||||
200,
|
||||
$"Scene {item.TemporarySceneNumber:N0} relative time",
|
||||
commitWarnings);
|
||||
@ -677,18 +612,18 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
||||
"dbo.Scene_Save",
|
||||
new
|
||||
{
|
||||
SceneID = item.SceneID ?? existingScene?.SceneID,
|
||||
SceneID = (int?)null,
|
||||
ChapterID = request.ChapterID,
|
||||
SceneNumber = Convert.ToDecimal(item.TemporarySceneNumber),
|
||||
SceneTitle = sceneTitle,
|
||||
Summary = item.Summary,
|
||||
TimeModeID = preserveExistingChronology ? existingScene!.TimeModeID : item.TimeModeID,
|
||||
StartDateTime = preserveExistingChronology ? existingScene!.StartDateTime : item.StartDateTime,
|
||||
EndDateTime = preserveExistingChronology ? existingScene!.EndDateTime : item.EndDateTime,
|
||||
DurationAmount = preserveExistingChronology ? existingScene!.DurationAmount : item.DurationAmount,
|
||||
DurationUnitID = preserveExistingChronology ? existingScene!.DurationUnitID : item.DurationUnitID,
|
||||
TimeModeID = request.TimeModeID,
|
||||
StartDateTime = (DateTime?)null,
|
||||
EndDateTime = (DateTime?)null,
|
||||
DurationAmount = (decimal?)null,
|
||||
DurationUnitID = (int?)null,
|
||||
RelativeTimeText = relativeTimeText,
|
||||
TimeConfidenceID = preserveExistingChronology ? existingScene!.TimeConfidenceID : item.TimeConfidenceID,
|
||||
TimeConfidenceID = request.TimeConfidenceID,
|
||||
ScenePurposeNotes = item.PurposeNotes,
|
||||
SceneOutcomeNotes = item.OutcomeNotes,
|
||||
RevisionStatusID = request.RevisionStatusID,
|
||||
@ -802,7 +737,7 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
||||
}
|
||||
|
||||
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(commitWarnings.Where(warning => !string.IsNullOrWhiteSpace(warning)));
|
||||
|
||||
@ -829,9 +764,7 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
||||
CommitID = commitId,
|
||||
ScenesCreated = scenesCreated,
|
||||
MetricsCreated = metricsCreated,
|
||||
Message = existingScenes.Count > 0
|
||||
? $"Enriched {scenesCreated:N0} existing scene(s)."
|
||||
: $"Created {scenesCreated:N0} scene(s)."
|
||||
Message = $"Created {scenesCreated:N0} scene(s)."
|
||||
};
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -23,7 +23,6 @@ public interface IStoryMemoryRepository
|
||||
Task UpsertAssignmentAsync(StoryMemoryIllustrationAssignmentSave request);
|
||||
Task InvalidateAssignmentAsync(int assignmentId, string reason);
|
||||
Task<StoryMemoryIllustrationDemand> UpsertDemandAsync(StoryMemoryIllustrationDemandSave request);
|
||||
Task<IReadOnlyList<StoryMemoryIllustrationDemand>> ListDemandsAsync(int importSessionId);
|
||||
Task<string> GetAppearancePreferenceAsync(int importSessionId);
|
||||
Task<StoryMemoryImportContext> GetImportContextAsync(int importSessionId);
|
||||
Task SetAppearancePreferenceAsync(int importSessionId, string preference);
|
||||
@ -364,18 +363,6 @@ public sealed class StoryMemoryRepository(ISqlConnectionFactory connectionFactor
|
||||
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)
|
||||
=> (await GetImportContextAsync(importSessionId)).AppearancePreference;
|
||||
|
||||
|
||||
@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -10,9 +10,9 @@ Replace the insertion blocks marked with `{{...}}` before sending.
|
||||
|
||||
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
|
||||
|
||||
@ -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.
|
||||
4. Do not split merely because there is a paragraph break.
|
||||
5. Do not try to create perfect scenes.
|
||||
6. Return strict JSON only.
|
||||
7. Do not wrap JSON in markdown fences.
|
||||
8. Do not include explanatory prose before or after the JSON object.
|
||||
6. Return JSON only.
|
||||
|
||||
## Boundary Guidance
|
||||
|
||||
@ -88,25 +86,9 @@ Each `startParagraph` and `endParagraph` must refer to those explicit paragraph
|
||||
Never invent paragraph numbers.
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
Scene boundaries must:
|
||||
@ -128,8 +110,6 @@ Return only these root properties:
|
||||
|
||||
- `schemaVersion`
|
||||
- `chapterSummary`
|
||||
- `defaultPov`
|
||||
- `mixedPov`
|
||||
- `sceneBoundaries`
|
||||
|
||||
Each `sceneBoundaries` item must contain exactly:
|
||||
@ -137,27 +117,10 @@ Each `sceneBoundaries` item must contain exactly:
|
||||
- `sceneNumber`
|
||||
- `startParagraph`
|
||||
- `endParagraph`
|
||||
- `suggestedTitle`
|
||||
- `structuralSummary`
|
||||
- `structuralSetting`
|
||||
- `povOverride`
|
||||
- `confidence`
|
||||
- `reason`
|
||||
|
||||
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:
|
||||
|
||||
@ -170,7 +133,7 @@ Do not output:
|
||||
- assets;
|
||||
- relationships;
|
||||
- metrics;
|
||||
- interpretive scene analysis;
|
||||
- scene summaries;
|
||||
- observations;
|
||||
- continuity warnings;
|
||||
- plot threads.
|
||||
@ -181,22 +144,11 @@ Do not output:
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"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": [
|
||||
{
|
||||
"sceneNumber": 1,
|
||||
"startParagraph": 1,
|
||||
"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,
|
||||
"reason": "Opening scene remains in the same place and time."
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ The response must match this shape:
|
||||
"schemaVersion": "22",
|
||||
"updates": [
|
||||
{
|
||||
"canonicalName": "Mara",
|
||||
"canonicalName": "Maggie",
|
||||
"aliases": ["Miss Jones"],
|
||||
"recurrenceSignal": "Appears again with the same role and appearance evidence.",
|
||||
"confidence": 0.82,
|
||||
@ -97,3 +97,4 @@ CURRENT_STORY_MEMORY_JSON:
|
||||
|
||||
SCENE_RESULT_ID:
|
||||
{{SCENE_RESULT_ID}}
|
||||
|
||||
|
||||
@ -556,13 +556,6 @@ The root object must not include scene-level thread candidates or continuity war
|
||||
{
|
||||
"characterA": "Mara",
|
||||
"characterB": "Elias",
|
||||
"proposalKind": "Development",
|
||||
"factualRelationshipType": null,
|
||||
"relationshipState": "Distrustful",
|
||||
"isReciprocal": false,
|
||||
"isKnownToOtherCharacter": true,
|
||||
"intensity": 5,
|
||||
"development": "Mara confronts Elias and starts doubting him.",
|
||||
"relationshipSignal": "mistrust",
|
||||
"evidence": "Mara questions Elias about what he is hiding.",
|
||||
"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`
|
||||
|
||||
|
||||
@ -268,15 +268,9 @@ Use neutral terms such as:
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
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`.
|
||||
|
||||
## Knowledge Change Rules
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ It should not:
|
||||
1. Treat the manuscript as the authority.
|
||||
2. Prefer omission over invention.
|
||||
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.
|
||||
6. Separate named locations from generic rooms.
|
||||
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.
|
||||
|
||||
The supplied `sceneContext` distinguishes established PlotDirector context from narrative context and the current manuscript source:
|
||||
|
||||
- `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:
|
||||
When `knownCharacters` is supplied in the scene context, use it only as identity context for names already detected before analysis:
|
||||
|
||||
- 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`;
|
||||
- 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.
|
||||
|
||||
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:
|
||||
|
||||
- `Mara leaves Lena behind.`
|
||||
- `Lena is recently left by Mara.`
|
||||
- `Lena is at the archive office.`
|
||||
- `Mara travels from Lena's location.`
|
||||
- `Beth leaves Maggie behind.`
|
||||
- `Maggie is recently left by Beth.`
|
||||
- `Maggie is at the Ashdown Trust.`
|
||||
- `Beth travels from Maggie's location.`
|
||||
|
||||
Good when supported by reflective or emotional text:
|
||||
|
||||
- `Mara notices Lena's absence.`
|
||||
- `Mara thinks about Lena.`
|
||||
- `Mara feels the car is too quiet without Lena.`
|
||||
- `Mara wonders whether she may be closer to finding Lena.`
|
||||
- `Lena is mentioned only in Mara's thoughts.`
|
||||
- `Beth notices Maggie's absence.`
|
||||
- `Beth thinks about Maggie.`
|
||||
- `Beth feels the car is too quiet without Maggie.`
|
||||
- `Beth wonders whether she may be closer to finding Maggie.`
|
||||
- `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.
|
||||
|
||||
@ -154,29 +142,70 @@ Use lower confidence when:
|
||||
- the scene relies on context not supplied;
|
||||
- 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
|
||||
|
||||
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`;
|
||||
- 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.
|
||||
Preferred V1 metrics:
|
||||
|
||||
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
|
||||
|
||||
@ -190,7 +219,7 @@ Report characters who are:
|
||||
For each character:
|
||||
|
||||
- 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`;
|
||||
- set `mentionedOnly` to `false` when active in the scene;
|
||||
- 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`.
|
||||
|
||||
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:
|
||||
|
||||
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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`;
|
||||
- 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.
|
||||
PlotDirector may resolve narrator identity later from chapter narrator metadata. Scene Intelligence should not force narrator identity where the scene does not provide it.
|
||||
|
||||
## Asset Reporting Rules
|
||||
|
||||
@ -289,7 +311,6 @@ Report an object as an asset when it is:
|
||||
- sought, lost, hidden, stolen or exchanged;
|
||||
- linked to a story question;
|
||||
- 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.
|
||||
|
||||
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 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
|
||||
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
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`.
|
||||
|
||||
## Knowledge Change Rules
|
||||
|
||||
@ -362,9 +375,7 @@ Rules:
|
||||
|
||||
- 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 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.
|
||||
- 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 `sourceType: null` when the source type is unsupported.
|
||||
- Keep `evidence` concise and scene-backed.
|
||||
@ -382,30 +393,12 @@ Report chronology clues such as:
|
||||
|
||||
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`.
|
||||
|
||||
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
|
||||
|
||||
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`.
|
||||
|
||||
## 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
|
||||
|
||||
Report answers supplied by the scene.
|
||||
@ -559,19 +485,19 @@ Observation rules:
|
||||
|
||||
Example source text:
|
||||
|
||||
`The car felt too quiet without Lena.`
|
||||
`The car felt too quiet without Maggie.`
|
||||
|
||||
Correct character extraction:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Lena",
|
||||
"name": "Maggie",
|
||||
"roleInScene": "mentioned",
|
||||
"mentionedOnly": true,
|
||||
"aliases": [],
|
||||
"actions": [],
|
||||
"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",
|
||||
"subjectEntityType": "Character",
|
||||
"subjectName": "Mara",
|
||||
"subjectName": "Narrator",
|
||||
"objectEntityType": "Character",
|
||||
"objectName": "Lena",
|
||||
"objectName": "Maggie",
|
||||
"predicate": "noticesAbsenceOf",
|
||||
"description": "Mara feels Lena's absence in the car.",
|
||||
"evidence": "The car felt too quiet without Lena.",
|
||||
"description": "The narrator feels Maggie's absence in the car.",
|
||||
"evidence": "The car felt too quiet without Maggie.",
|
||||
"confidence": 0.9
|
||||
}
|
||||
```
|
||||
|
||||
Incorrect:
|
||||
|
||||
- `Mara leaves Lena behind.`
|
||||
- `Lena was recently left by Mara.`
|
||||
- `Lena is located near the archive office.`
|
||||
- `Beth leaves Maggie behind.`
|
||||
- `Maggie was recently left by Beth.`
|
||||
- `Maggie is located near the Ashdown Trust.`
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@ -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.
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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.
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user