dsh-openwrite 0.2.27 → 0.2.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +55 -6
- package/THIRD_PARTY_NOTICES.md +4 -3
- package/companions.mjs +508 -0
- package/cordis.patch.yml +0 -2
- package/docs/CLOSED_ISSUE_FOLLOWUPS.md +5 -3
- package/docs/COMPANION_PLUGINS.md +142 -0
- package/docs/CONTEXT_SLOTS.md +60 -0
- package/docs/COST_MEASUREMENT.md +2 -2
- package/docs/DEVELOPMENT.md +6 -11
- package/docs/DSH_STORE.md +14 -2
- package/docs/EDITOR_SAFETY_REPAIRS.md +17 -0
- package/docs/INSTALL.md +112 -4
- package/docs/ISSUES_121_123.md +97 -0
- package/docs/ISSUES_140_142.md +58 -0
- package/docs/ISSUES_149_150_151.md +77 -0
- package/docs/ISSUES_98_100.md +2 -0
- package/docs/ISSUE_123_FOLLOWUP.md +55 -0
- package/docs/ISSUE_146_REVIEW_EVIDENCE.md +53 -0
- package/docs/LEARNING.md +1 -1
- package/docs/MOBILE_READINESS.md +62 -0
- package/docs/PLUGIN_MAINTENANCE.md +1 -3
- package/docs/RELEASE_ACCEPTANCE.md +90 -3
- package/docs/REVIEW_ROUND_22.md +67 -0
- package/docs/REVIEW_ROUND_23.md +48 -0
- package/docs/REVIEW_ROUND_25.md +51 -0
- package/docs/SELECTION_UPGRADE_REPAIRS.md +16 -0
- package/docs/TOOLS.md +933 -0
- package/docs/WORKFLOWS.md +5 -5
- package/package.json +18 -21
- package/packages/openwrite-bridge/lib/client.d.ts +0 -1
- package/packages/openwrite-bridge/lib/client.js +90 -9
- package/packages/openwrite-bridge/lib/contracts-generated.js +2 -1
- package/packages/openwrite-bridge/lib/dog-delivery.d.ts +1 -0
- package/packages/openwrite-bridge/lib/dog-delivery.js +10 -4
- package/packages/openwrite-bridge/lib/dog-review.js +15 -3
- package/packages/openwrite-bridge/lib/domain.d.ts +27 -0
- package/packages/openwrite-bridge/lib/domain.js +250 -29
- package/packages/openwrite-bridge/lib/host-llm.d.ts +188 -0
- package/packages/openwrite-bridge/lib/host-llm.js +657 -0
- package/packages/openwrite-bridge/lib/host-web.d.ts +61 -0
- package/packages/openwrite-bridge/lib/host-web.js +166 -0
- package/packages/openwrite-bridge/lib/index.d.ts +4 -0
- package/packages/openwrite-bridge/lib/index.js +150 -6
- package/packages/openwrite-bridge/lib/managed-runtime.d.ts +47 -2
- package/packages/openwrite-bridge/lib/managed-runtime.js +179 -35
- package/packages/openwrite-bridge/lib/tool-policy.js +39 -12
- package/packages/openwrite-bridge/lib/tools.js +1238 -1219
- package/packages/openwrite-bridge/package.json +7 -5
- package/packages/studio-panel/lib/client.js +116667 -114773
- package/packages/studio-panel/lib/client.js.map +1 -1
- package/packages/studio-panel/lib/types/client/LaunchOpenWrite.d.ts +10 -0
- package/packages/studio-panel/lib/types/client/ModelView.d.ts +28 -4
- package/packages/studio-panel/lib/types/client/OpenWriteSettings.d.ts +64 -0
- package/packages/studio-panel/lib/types/client/SceneWorkbench.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/SearchSettings.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/SerialWritingView.d.ts +2 -1
- package/packages/studio-panel/lib/types/client/TurnMutationSummary.d.ts +2 -0
- package/packages/studio-panel/lib/types/client/VditorBody.d.ts +8 -2
- package/packages/studio-panel/lib/types/client/api.d.ts +41 -2
- package/packages/studio-panel/lib/types/client/browser-download.d.ts +4 -0
- package/packages/studio-panel/lib/types/client/companions.d.ts +46 -0
- package/packages/studio-panel/lib/types/client/dto.d.ts +22 -0
- package/packages/studio-panel/lib/types/client/locales.d.ts +1 -1
- package/packages/studio-panel/lib/types/client/manuscript-annotations.d.ts +2 -1
- package/packages/studio-panel/lib/types/client/manuscript-overlay.d.ts +2 -1
- package/packages/studio-panel/lib/types/client/manuscript-selection.d.ts +18 -5
- package/packages/studio-panel/lib/types/client/selection-coordinates.d.ts +15 -0
- package/packages/studio-panel/lib/types/client/workspace-chrome.d.ts +41 -0
- package/packages/studio-panel/package.json +5 -3
- package/plugin.mjs +159 -20
- package/preset-definition.mjs +144 -4
- package/presets/openwrite/agent.cordis.yml +7 -9
- package/presets/openwrite/ops-integration.mjs +28 -0
- package/presets/openwrite/preset.yml +2 -1
- package/presets/openwrite/skills/canon/SKILL.md +3 -3
- package/presets/openwrite/skills/delivery-artifact-query/SKILL.md +41 -0
- package/presets/openwrite/skills/dialoguequality/SKILL.md +16 -17
- package/presets/openwrite/skills/dog-delivery-verification/SKILL.md +113 -0
- package/presets/openwrite/skills/export-book/SKILL.md +3 -3
- package/presets/openwrite/skills/foreshadow/SKILL.md +1 -1
- package/presets/openwrite/skills/foreshadowing-system/SKILL.md +16 -25
- package/presets/openwrite/skills/import-artifact-query/SKILL.md +30 -0
- package/presets/openwrite/skills/learn/SKILL.md +3 -3
- package/presets/openwrite/skills/novel-creator/SKILL.md +10 -10
- package/presets/openwrite/skills/novel-reviewer/SKILL.md +11 -11
- package/presets/openwrite/skills/progress/SKILL.md +1 -1
- package/presets/openwrite/skills/review-artifact-query/SKILL.md +41 -0
- package/presets/openwrite/skills/review-chapter/SKILL.md +2 -2
- package/presets/openwrite/skills/revise-span/SKILL.md +5 -5
- package/presets/openwrite/skills/style-system/SKILL.md +13 -6
- package/presets/openwrite/skills/truth-validation/SKILL.md +9 -12
- package/presets/openwrite/skills/workflow-manager/SKILL.md +13 -13
- package/presets/openwrite/skills/world-query/SKILL.md +16 -11
- package/presets/openwrite/skills/write-next/SKILL.md +1 -1
- package/release/{openwrite-5.8.16-py3-none-any.whl → openwrite-5.8.30-py3-none-any.whl} +0 -0
- package/release/runtime-manifest.json +4 -12
- package/release/source.json +21 -21
- package/release/wheels/licenses/OPENSSL-3.6.3-LICENSE.txt +225 -0
- package/scripts/maintenance.mjs +67 -7
- package/scripts/plugin-doctor.mjs +179 -0
- package/scripts/prepare.mjs +11 -2
- package/scripts/safe-upgrade.mjs +141 -13
- package/dog-tools.mjs +0 -11
- package/presets/openwrite/skills/dog-delivery-query/SKILL.md +0 -42
- package/presets/openwrite/skills/dog-import-query/SKILL.md +0 -29
- package/presets/openwrite/skills/dog-review-query/SKILL.md +0 -42
- package/scripts/dog/delivery-stage.js +0 -32
- package/scripts/dog/import-chapter.js +0 -20
- package/scripts/dog/import-record.js +0 -35
- package/scripts/dog/review-dimension.js +0 -41
- package/scripts/dog/review-record.js +0 -42
- package/vendor/dsh-dog/LICENSE +0 -29
- package/vendor/dsh-dog/README.md +0 -114
- package/vendor/dsh-dog/UPSTREAM.md +0 -30
- package/vendor/dsh-dog/cordis.patch.yml +0 -14
- package/vendor/dsh-dog/lib/client.js +0 -2960
- package/vendor/dsh-dog/lib/client.js.map +0 -1
- package/vendor/dsh-dog/lib/core-BAlgZpLq.js +0 -2074
- package/vendor/dsh-dog/lib/core-BiEBMLz3.d.ts +0 -495
- package/vendor/dsh-dog/lib/core.d.ts +0 -2
- package/vendor/dsh-dog/lib/core.js +0 -2
- package/vendor/dsh-dog/lib/index.d.ts +0 -143
- package/vendor/dsh-dog/lib/index.js +0 -1279
- package/vendor/dsh-dog/package.json +0 -110
- package/vendor/dsh-dog/schemas/schema-0.2/graph.schema.json +0 -139
- package/vendor/dsh-dog/schemas/schema-0.2/report.schema.json +0 -87
- package/vendor/dsh-dog/schemas/schema-0.2/run.schema.json +0 -259
- package/vendor/dsh-dog/schemas/schema-0.2/runtime-event.schema.json +0 -105
- package/vendor/dsh-dog/schemas/schema-0.2/verification.schema.json +0 -40
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Offline maintenance checks. Never reads credentials, starts a model, or edits profiles.
|
|
3
|
+
import { readFile, realpath } from 'node:fs/promises'
|
|
4
|
+
import { execFileSync } from 'node:child_process'
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
6
|
+
import { isAbsolute, join, resolve } from 'node:path'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { load } from 'js-yaml'
|
|
9
|
+
import { inspectProfile } from './maintenance.mjs'
|
|
10
|
+
import { detectCompanions } from '../companions.mjs'
|
|
11
|
+
|
|
12
|
+
const root = fileURLToPath(new URL('../', import.meta.url))
|
|
13
|
+
const packageDirs = ['.', 'packages/openwrite-bridge', 'packages/studio-panel']
|
|
14
|
+
const isDsh = name => /^@deepseek-ai\/dsh(?:-|$)/.test(name)
|
|
15
|
+
const lockName = path => path.match(/(?:^|\/)node_modules\/(@deepseek-ai\/[^/]+)$/)?.[1]
|
|
16
|
+
const readJson = async path => JSON.parse(await readFile(path, 'utf8'))
|
|
17
|
+
const exactVersion = value => /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(value ?? '')
|
|
18
|
+
|
|
19
|
+
export function auditVersions(manifest, lock, baseline, graph = {}) {
|
|
20
|
+
const errors = []
|
|
21
|
+
if (!exactVersion(baseline)) errors.push('DSH baseline must be an exact version')
|
|
22
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
23
|
+
const declared = manifest[field] ?? {}
|
|
24
|
+
const locked = lock.packages?.['']?.[field] ?? {}
|
|
25
|
+
for (const name of new Set([...Object.keys(declared), ...Object.keys(locked)])) {
|
|
26
|
+
if (declared[name] !== locked[name]) errors.push(`lock metadata differs: ${field}.${name}`)
|
|
27
|
+
// Build dependencies remain pinned; public peers may enumerate verified hosts.
|
|
28
|
+
const peerVersions = typeof declared[name] === 'string' ? declared[name].split(' || ') : []
|
|
29
|
+
const verified = manifest.dsh?.compatibility?.dshReleases ?? {}
|
|
30
|
+
const compatiblePeer = field === 'peerDependencies' && peerVersions.length > 0
|
|
31
|
+
&& peerVersions.includes(graph[name] ?? baseline)
|
|
32
|
+
&& peerVersions.every(value => exactVersion(value)
|
|
33
|
+
&& (value === (graph[name] ?? baseline) || verified[value] === 'compatible'))
|
|
34
|
+
if (isDsh(name) && declared[name] !== undefined && declared[name] !== (graph[name] ?? baseline) && !compatiblePeer) {
|
|
35
|
+
errors.push(`unpinned DSH dependency: ${name}=${declared[name]}`)
|
|
36
|
+
}
|
|
37
|
+
if (isDsh(name) && declared[name] !== undefined
|
|
38
|
+
&& (field !== 'peerDependencies' || !manifest.peerDependenciesMeta?.[name]?.optional)
|
|
39
|
+
&& !lock.packages?.[`node_modules/${name}`]) {
|
|
40
|
+
errors.push(`required SDK missing from lock: ${name}`)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const [path, entry] of Object.entries(lock.packages ?? {})) {
|
|
45
|
+
const name = lockName(path)
|
|
46
|
+
if (name && isDsh(name) && entry.version !== (graph[name] ?? baseline)) {
|
|
47
|
+
errors.push(`DSH lock drift: ${name}=${entry.version}, expected ${baseline}`)
|
|
48
|
+
}
|
|
49
|
+
if (name === '@deepseek-ai/cordis' && entry.version !== (graph[name] ?? '4.0.2')) {
|
|
50
|
+
errors.push(`Cordis lock drift: ${entry.version}, expected ${graph[name] ?? '4.0.2'}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!lock.packages?.['']) errors.push('missing lockfile root metadata')
|
|
54
|
+
return errors
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const exportPaths = value => typeof value === 'string' ? [value]
|
|
58
|
+
: value && typeof value === 'object' ? Object.values(value).flatMap(exportPaths) : []
|
|
59
|
+
const packagePath = path => path?.replace(/^\.\//, '')
|
|
60
|
+
|
|
61
|
+
export function auditBundle(manifest, files, patch) {
|
|
62
|
+
const errors = []
|
|
63
|
+
const available = new Set(files)
|
|
64
|
+
const required = [manifest.main, manifest.types, manifest.dsh?.bundle?.patch,
|
|
65
|
+
...exportPaths(manifest.exports)]
|
|
66
|
+
if (!manifest.dsh?.bundle?.patch) errors.push('missing dsh.bundle.patch')
|
|
67
|
+
if (!manifest.main || !manifest.types) errors.push('missing host JS/types entry')
|
|
68
|
+
for (const path of required.filter(Boolean)) {
|
|
69
|
+
if (isAbsolute(path) || path.split(/[\\/]/).includes('..') || path.includes('\\')) {
|
|
70
|
+
errors.push(`entry escapes package: ${path}`)
|
|
71
|
+
} else if (!available.has(packagePath(path))) errors.push(`entry missing from tarball: ${path}`)
|
|
72
|
+
}
|
|
73
|
+
const rows = Array.isArray(patch) ? patch.flatMap(op => op?.insert ?? []) : []
|
|
74
|
+
if (rows.filter(row => row.name === manifest.name).length !== 1) {
|
|
75
|
+
errors.push('bundle patch must mount its host package exactly once')
|
|
76
|
+
}
|
|
77
|
+
const ids = rows.map(row => row.id)
|
|
78
|
+
if (ids.some(id => typeof id !== 'string' || !id) || new Set(ids).size !== ids.length) {
|
|
79
|
+
errors.push('bundle patch has missing/duplicate row ids')
|
|
80
|
+
}
|
|
81
|
+
if (manifest.dsh?.client || manifest.exports?.['./client']) {
|
|
82
|
+
if (manifest.dsh?.client?.platform !== 'web' || !manifest.exports?.['./client']) {
|
|
83
|
+
errors.push('web client needs platform=web and ./client export')
|
|
84
|
+
}
|
|
85
|
+
// The host route serves these on demand; a source-tree-only build can mask omissions.
|
|
86
|
+
for (const path of ['LICENSE', 'dist/index.css', 'dist/index.min.js',
|
|
87
|
+
'dist/css/content-theme/dark.css', 'dist/css/content-theme/light.css',
|
|
88
|
+
'dist/js/lute/lute.min.js', 'dist/js/icons/ant.js', 'dist/js/i18n/zh_CN.js']) {
|
|
89
|
+
if (!available.has(`vendor/vditor/${path}`)) errors.push(`editor asset missing from tarball: ${path}`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return errors
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function checkProfiles(report) {
|
|
96
|
+
const home = process.env.DSH_HOME ?? join(process.env.HOME ?? '', '.dsh')
|
|
97
|
+
for (const profile of ['web', 'headless']) {
|
|
98
|
+
const dir = join(home, 'profiles', profile)
|
|
99
|
+
await report(`profile ${profile}`, async () => {
|
|
100
|
+
const manifest = await readJson(join(dir, 'package.json'))
|
|
101
|
+
const name = 'dsh-openwrite'
|
|
102
|
+
if (manifest.dsh?.profile?.bundles?.filter(n => n === name).length !== 1) throw new Error('standard bundle not installed exactly once')
|
|
103
|
+
const installed = await realpath(join(dir, 'node_modules', name))
|
|
104
|
+
const pkg = await readJson(join(installed, 'package.json'))
|
|
105
|
+
if (pkg.name !== name || !pkg.dsh?.bundle?.patch) throw new Error('invalid installed bundle')
|
|
106
|
+
await readFile(join(installed, 'release/runtime-manifest.json'))
|
|
107
|
+
const leftover = inspectProfile(manifest).leftover
|
|
108
|
+
if (leftover.length) throw new Error(`legacy packages still in bundles or dependencies (${leftover.join(', ')}); run openwrite-maintenance migrate --apply with dsh stopped`)
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function doctor({ profiles = false } = {}) {
|
|
114
|
+
let failures = 0
|
|
115
|
+
const report = async (label, check) => {
|
|
116
|
+
try { await check(); console.log(`PASS ${label}`) }
|
|
117
|
+
catch (error) { failures++; console.error(`FAIL ${label}: ${error.message}`) }
|
|
118
|
+
}
|
|
119
|
+
const assertClean = errors => { if (errors.length) throw new Error(errors.slice(0, 8).join('; ') + (errors.length > 8 ? `; +${errors.length - 8} more` : '')) }
|
|
120
|
+
const rootManifest = await readJson(join(root, 'package.json'))
|
|
121
|
+
const compatibility = await readJson(join(root, 'release/sdk-compatibility.json'))
|
|
122
|
+
const baseline = rootManifest.devDependencies?.['@deepseek-ai/dsh']
|
|
123
|
+
console.log(`dsh-Openwrite doctor — DSH ${baseline}, Cordis 4.0.2 (offline)`)
|
|
124
|
+
await report('Node >= 22.19.0', () => {
|
|
125
|
+
const [major, minor] = process.versions.node.split('.').map(Number)
|
|
126
|
+
if (major < 22 || (major === 22 && minor < 19)) throw new Error(`found ${process.versions.node}`)
|
|
127
|
+
})
|
|
128
|
+
for (const relative of packageDirs) {
|
|
129
|
+
const dir = resolve(root, relative)
|
|
130
|
+
const manifest = await readJson(join(dir, 'package.json'))
|
|
131
|
+
const lock = await readJson(join(dir, 'package-lock.json'))
|
|
132
|
+
await report(`${relative}: manifest + lock compatibility`, () => assertClean(auditVersions(manifest, lock, baseline, compatibility.packages)))
|
|
133
|
+
await report(`${relative}: installed SDK matches lock`, async () => {
|
|
134
|
+
const errors = []
|
|
135
|
+
for (const [path, entry] of Object.entries(lock.packages ?? {})) {
|
|
136
|
+
const name = lockName(path)
|
|
137
|
+
if (!name || (!isDsh(name) && name !== '@deepseek-ai/cordis')) continue
|
|
138
|
+
try {
|
|
139
|
+
const installed = await readJson(join(dir, path, 'package.json'))
|
|
140
|
+
if (installed.version !== entry.version) errors.push(`${name}=${installed.version}, lock=${entry.version}`)
|
|
141
|
+
} catch { errors.push(`missing ${name}; run npm ci in ${relative}`) }
|
|
142
|
+
}
|
|
143
|
+
assertClean(errors)
|
|
144
|
+
})
|
|
145
|
+
if (relative === '.') continue
|
|
146
|
+
await report(`${manifest.name}: packed entries + patch + assets`, async () => {
|
|
147
|
+
const packed = JSON.parse(execFileSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts'],
|
|
148
|
+
{ cwd: dir, encoding: 'utf8', timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'] }))
|
|
149
|
+
const patch = load(await readFile(join(dir, manifest.dsh.bundle.patch), 'utf8'))
|
|
150
|
+
assertClean(auditBundle(manifest, packed[0].files.map(file => file.path), patch))
|
|
151
|
+
const host = await import(pathToFileURL(join(dir, manifest.main)).href)
|
|
152
|
+
if (host.name !== manifest.name || typeof host.apply !== 'function') throw new Error('host must export name + apply')
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
if (profiles) await checkProfiles(report)
|
|
156
|
+
await report('companion plugins', async () => {
|
|
157
|
+
const home = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
|
|
158
|
+
const companions = await detectCompanions(home)
|
|
159
|
+
const lines = []
|
|
160
|
+
if (companions.dog.installed) lines.push(`dsh-dog ${companions.dog.usable ? 'active' : `inactive (${companions.dog.reason})`}`)
|
|
161
|
+
if (companions.ops.installed) lines.push(`dsh-ops ${companions.ops.usable ? 'active' : `inactive (${companions.ops.reason})`}`)
|
|
162
|
+
if (companions.freeModel.installed) lines.push(`dsh-our-free-model ${companions.freeModel.usable ? 'active' : `inactive (${companions.freeModel.reason})`}`)
|
|
163
|
+
if (companions.host === null && (companions.dog.installed || companions.ops.installed || companions.freeModel.installed)) {
|
|
164
|
+
throw new Error(`host version unresolved; companion decisions stay fail-closed (${lines.join(', ')})`)
|
|
165
|
+
}
|
|
166
|
+
console.log(` ${lines.join(', ') || 'none installed'}`)
|
|
167
|
+
})
|
|
168
|
+
console.log(failures ? `${failures} check(s) failed.` : 'All checks passed. No model or live-service call was made.')
|
|
169
|
+
return failures ? 1 : 0
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
173
|
+
const unknown = process.argv.slice(2).filter(arg => arg !== '--profiles')
|
|
174
|
+
if (unknown.length) { console.error('Usage: node scripts/plugin-doctor.mjs [--profiles]'); process.exitCode = 2 }
|
|
175
|
+
else {
|
|
176
|
+
try { process.exitCode = await doctor({ profiles: process.argv.includes('--profiles') }) }
|
|
177
|
+
catch (error) { console.error(`FAIL ${error.message}`); process.exitCode = 1 }
|
|
178
|
+
}
|
|
179
|
+
}
|
package/scripts/prepare.mjs
CHANGED
|
@@ -7,7 +7,7 @@ const root = fileURLToPath(new URL('../', import.meta.url))
|
|
|
7
7
|
const manifest = JSON.parse(await readFile(new URL('../release/runtime-manifest.json', import.meta.url), 'utf8'))
|
|
8
8
|
let built = false
|
|
9
9
|
await access(new URL('../release/' + manifest.wheel.file, import.meta.url))
|
|
10
|
-
for (const dir of ['packages/openwrite-bridge', 'packages/studio-panel'
|
|
10
|
+
for (const dir of ['packages/openwrite-bridge', 'packages/studio-panel']) {
|
|
11
11
|
try { await access(new URL(`../${dir}/lib/index.js`, import.meta.url)); await access(new URL(`../${dir}/lib/${dir.endsWith('openwrite-bridge') ? 'preset-tools.js' : 'client.js'}`, import.meta.url)); continue } catch {}
|
|
12
12
|
built = true
|
|
13
13
|
for (const args of [['ci', '--ignore-scripts', '--no-audit', '--no-fund'], ['run', 'build']]) {
|
|
@@ -15,7 +15,16 @@ for (const dir of ['packages/openwrite-bridge', 'packages/studio-panel', 'vendor
|
|
|
15
15
|
if (result.status !== 0) throw new Error(`${dir}: ${args.join(' ')} failed`)
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
// Provenance must be refreshed whenever the source tree no longer matches the
|
|
19
|
+
// recorded hash, not merely when this run happened to build. Gating on `built`
|
|
20
|
+
// alone means "edit src/, then pack without rebuilding" publishes a tarball
|
|
21
|
+
// whose release/source.json still names the previous tree — and that file is
|
|
22
|
+
// gitignored, so release-report.mjs's dirty check cannot see it either.
|
|
23
|
+
let provenanceStale = true
|
|
24
|
+
try {
|
|
25
|
+
provenanceStale = !JSON.parse(await readFile(new URL('../release/source.json', import.meta.url), 'utf8')).sourceTreeSha256
|
|
26
|
+
} catch { /* missing or unreadable: regenerate */ }
|
|
27
|
+
if (built || provenanceStale) {
|
|
19
28
|
const result = spawnSync(process.execPath, [fileURLToPath(new URL('./release-provenance.mjs', import.meta.url))], { cwd: root, stdio: 'inherit' })
|
|
20
29
|
if (result.status !== 0) throw new Error('Release provenance generation failed')
|
|
21
30
|
}
|
package/scripts/safe-upgrade.mjs
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Offline profile upgrades retain the previous complete dependency tree until commit.
|
|
3
|
-
import { cp, mkdir, readFile, readdir, rename, writeFile, open, access, readlink, symlink, unlink } from 'node:fs/promises'
|
|
3
|
+
import { cp, mkdir, readFile, readdir, rename, writeFile, open, access, readlink, symlink, unlink, realpath, lstat, rmdir, rm, utimes } from 'node:fs/promises'
|
|
4
4
|
import { spawn, spawnSync } from 'node:child_process'
|
|
5
5
|
import { join, resolve, relative, isAbsolute, dirname, sep } from 'node:path'
|
|
6
6
|
import { randomUUID, createHash } from 'node:crypto'
|
|
7
7
|
import { homedir } from 'node:os'
|
|
8
8
|
import { pathToFileURL } from 'node:url'
|
|
9
|
-
import {
|
|
10
|
-
import yaml from 'js-yaml'
|
|
9
|
+
import { createRequire } from 'node:module'
|
|
11
10
|
|
|
12
11
|
const PROFILE = /^[a-zA-Z0-9_-]+$/
|
|
13
12
|
const digest = bytes => createHash('sha256').update(bytes).digest('hex')
|
|
13
|
+
function upgradeDependencies(home, profile) {
|
|
14
|
+
// Release archives do not contain node_modules. Resolve the same dependencies
|
|
15
|
+
// from the explicitly selected installed profile, without loading its plugin.
|
|
16
|
+
const resolvers = [createRequire(import.meta.url), createRequire(join(profileDirectory(home, profile), 'package.json'))]
|
|
17
|
+
const load = name => {
|
|
18
|
+
for (const require of resolvers) {
|
|
19
|
+
try { return require(name) } catch (error) {
|
|
20
|
+
if (error.code !== 'MODULE_NOT_FOUND') throw error
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`UPGRADE_DEPENDENCY_MISSING: ${name}; run this tool from an installed OpenWrite package or repair dependencies in the selected profile first`)
|
|
24
|
+
}
|
|
25
|
+
return { acquireLock: load('proper-lockfile').lock, yaml: load('js-yaml') }
|
|
26
|
+
}
|
|
14
27
|
async function exists(path) { try { await access(path); return true } catch (error) { if (error.code === 'ENOENT') return false; throw error } }
|
|
15
28
|
async function save(path, value) {
|
|
16
29
|
const pending = path + '.pending-' + randomUUID()
|
|
@@ -18,7 +31,70 @@ async function save(path, value) {
|
|
|
18
31
|
await rename(pending, path)
|
|
19
32
|
}
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
// Recovery cannot load dependencies through a profile which is between renames.
|
|
35
|
+
// Use proper-lockfile's same canonical <profiles>.lock directory and fresh mtime
|
|
36
|
+
// protocol, without importing any code from the missing or backup profile.
|
|
37
|
+
export async function acquireRecoveryLock(home) {
|
|
38
|
+
const path = (await realpath(join(resolve(home), 'profiles'))) + '.lock'
|
|
39
|
+
const locked = () => Object.assign(new Error('ELOCKED: another profile operation holds the upgrade lock'), { code: 'ELOCKED' })
|
|
40
|
+
const same = (a, b) => a.dev === b.dev && a.ino === b.ino && a.birthtimeMs === b.birthtimeMs
|
|
41
|
+
let owner
|
|
42
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
43
|
+
try { await mkdir(path, { mode: 0o700 }); owner = await lstat(path); break }
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code !== 'EEXIST') throw error
|
|
46
|
+
let observed
|
|
47
|
+
try { observed = await lstat(path) } catch (cause) { if (cause.code === 'ENOENT') continue; throw cause }
|
|
48
|
+
if (!observed.isDirectory() || Date.now() - observed.mtimeMs <= 30_000) throw locked()
|
|
49
|
+
const current = await lstat(path)
|
|
50
|
+
if (!same(observed, current) || observed.mtimeMs !== current.mtimeMs) throw locked()
|
|
51
|
+
try { await rmdir(path) } catch (cause) { if (cause.code !== 'ENOENT') throw locked() }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!owner) throw locked()
|
|
55
|
+
let pending = Promise.resolve(), compromised
|
|
56
|
+
const assertOwned = async () => {
|
|
57
|
+
if (compromised) throw compromised
|
|
58
|
+
const current = await lstat(path)
|
|
59
|
+
if (!same(owner, current)) throw locked()
|
|
60
|
+
}
|
|
61
|
+
const timer = setInterval(() => {
|
|
62
|
+
pending = pending.then(async () => {
|
|
63
|
+
await assertOwned()
|
|
64
|
+
const now = new Date()
|
|
65
|
+
await utimes(path, now, now)
|
|
66
|
+
}).catch(error => { compromised = error })
|
|
67
|
+
}, 1000)
|
|
68
|
+
timer.unref()
|
|
69
|
+
return {
|
|
70
|
+
async assertOwned() { await pending; await assertOwned() },
|
|
71
|
+
async release() {
|
|
72
|
+
clearInterval(timer)
|
|
73
|
+
await pending
|
|
74
|
+
await assertOwned()
|
|
75
|
+
await rmdir(path)
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function pinArchive(home, archive) {
|
|
81
|
+
const bytes = await readFile(archive)
|
|
82
|
+
const sha256 = digest(bytes)
|
|
83
|
+
const cache = join(resolve(home), 'openwrite', 'packages')
|
|
84
|
+
await mkdir(cache, { recursive: true, mode: 0o700 })
|
|
85
|
+
const destination = join(cache, sha256 + '.tgz')
|
|
86
|
+
if (await exists(destination) && digest(await readFile(destination)) === sha256) return { path: destination, sha256 }
|
|
87
|
+
const pending = join(cache, '.' + sha256 + '-' + randomUUID() + '.pending')
|
|
88
|
+
try {
|
|
89
|
+
const handle = await open(pending, 'wx', 0o600)
|
|
90
|
+
try { await handle.writeFile(bytes); await handle.sync() } finally { await handle.close() }
|
|
91
|
+
if (digest(await readFile(pending)) !== sha256) throw new Error('PACKAGE_CACHE_CONFLICT: temporary archive verification failed')
|
|
92
|
+
await rename(pending, destination)
|
|
93
|
+
return { path: destination, sha256 }
|
|
94
|
+
} finally { try { await unlink(pending) } catch (error) { if (error.code !== 'ENOENT') throw error } }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function relocateProfileLinks(staging, previous, replacementRoot, yaml) {
|
|
22
98
|
const visit = async directory => {
|
|
23
99
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
24
100
|
const path = join(directory, entry.name)
|
|
@@ -87,6 +163,16 @@ async function runInstall(home, profile, spec, log) {
|
|
|
87
163
|
} finally { clearTimeout(timer); clearTimeout(drainTimer); clearTimeout(escalation); await writes }
|
|
88
164
|
}
|
|
89
165
|
|
|
166
|
+
/**
|
|
167
|
+
* `.plugin-manager` is host-owned state, not ours: it must never be staged, or a
|
|
168
|
+
* POSIX upgrade would commit a snapshot of it that later upgrades then restore.
|
|
169
|
+
* Compared on a normalized relative path so the check is separator-agnostic —
|
|
170
|
+
* `path.join` yields `\` on win32, which a literal '/' test can never match.
|
|
171
|
+
*/
|
|
172
|
+
function isHostManagedState(relativePath) {
|
|
173
|
+
return relativePath === '.plugin-manager' || relativePath.startsWith(`.plugin-manager${sep}`)
|
|
174
|
+
}
|
|
175
|
+
|
|
90
176
|
function profileDirectory(home, profile) {
|
|
91
177
|
if (!PROFILE.test(profile)) throw new Error('Invalid profile name')
|
|
92
178
|
return join(resolve(home), 'profiles', profile)
|
|
@@ -99,10 +185,15 @@ export async function recoverUpgrade(home, operationId, options = {}) {
|
|
|
99
185
|
const operation = JSON.parse(await readFile(join(root, 'operation.json'), 'utf8'))
|
|
100
186
|
const target = profileDirectory(home, operation.profile)
|
|
101
187
|
await (options.assertStopped || assertHostStopped)(home, operation.profile)
|
|
102
|
-
|
|
103
|
-
|
|
188
|
+
|
|
189
|
+
const previous = join(root, 'previous-profile')
|
|
190
|
+
const lease = await acquireRecoveryLock(home)
|
|
104
191
|
try {
|
|
105
192
|
if (!await exists(target) && await exists(previous)) {
|
|
193
|
+
if (!operation.old_manifest_sha256 || digest(await readFile(join(previous, 'package.json'))) !== operation.old_manifest_sha256) {
|
|
194
|
+
throw new Error('PROFILE_CHANGED: backup manifest differs from the approved operation; no profile restored')
|
|
195
|
+
}
|
|
196
|
+
await lease.assertOwned()
|
|
106
197
|
await rename(previous, target)
|
|
107
198
|
operation.status = 'rolled_back'
|
|
108
199
|
} else if (await exists(target)) {
|
|
@@ -111,17 +202,22 @@ export async function recoverUpgrade(home, operationId, options = {}) {
|
|
|
111
202
|
else if (actual === operation.old_manifest_sha256) operation.status = 'rolled_back'
|
|
112
203
|
else throw new Error('PROFILE_CHANGED: preserve the backup and reconcile the profile manually')
|
|
113
204
|
} else throw new Error('RECOVERY_REQUIRED: neither active nor previous profile is available')
|
|
205
|
+
await lease.assertOwned()
|
|
114
206
|
await save(join(root, 'operation.json'), operation)
|
|
115
207
|
return { operation_id: operationId, status: operation.status, backup: previous }
|
|
116
|
-
} finally { await release() }
|
|
208
|
+
} finally { await lease.release() }
|
|
117
209
|
}
|
|
118
210
|
|
|
119
211
|
/** Stage a complete profile beside the active tree, validate, then swap while stopped. */
|
|
120
212
|
export async function upgradeProfile(home, profile, spec, options = {}) {
|
|
121
213
|
if (typeof spec !== 'string' || !spec || spec.startsWith('-') || /[\r\n]/.test(spec)) throw new Error('A package spec or Release tarball is required')
|
|
214
|
+
// A bare add can preserve a profile's old exact dependency/lock selection
|
|
215
|
+
// even after npm latest moved (#142). An upgrade must request a new version.
|
|
216
|
+
if (spec === 'dsh-openwrite') spec = 'dsh-openwrite@latest'
|
|
122
217
|
const target = profileDirectory(home, profile)
|
|
123
218
|
const stopped = options.assertStopped || assertHostStopped
|
|
124
219
|
await stopped(home, profile)
|
|
220
|
+
const { acquireLock, yaml } = upgradeDependencies(home, profile)
|
|
125
221
|
const release = await acquireLock(join(home, 'profiles'), { realpath: true, retries: 0 })
|
|
126
222
|
const id = 'upgrade_' + randomUUID()
|
|
127
223
|
const root = join(resolve(home), 'openwrite', 'upgrades', id)
|
|
@@ -140,9 +236,21 @@ export async function upgradeProfile(home, profile, spec, options = {}) {
|
|
|
140
236
|
operation.old_manifest_sha256 = digest(oldManifest)
|
|
141
237
|
await save(join(root, 'operation.json'), operation)
|
|
142
238
|
await cp(target, staging, { recursive: true, dereference: false, verbatimSymlinks: true,
|
|
143
|
-
filter: source => !
|
|
144
|
-
await relocateProfileLinks(staging, target)
|
|
145
|
-
|
|
239
|
+
filter: source => !isHostManagedState(relative(target, source)) })
|
|
240
|
+
await relocateProfileLinks(staging, target, staging, yaml)
|
|
241
|
+
// A downloaded tarball may live in a disposable directory. Pin its bytes
|
|
242
|
+
// below DSH_HOME before the installer writes file: references into locks.
|
|
243
|
+
let installSpec = spec
|
|
244
|
+
const localPath = spec.startsWith('file:') ? spec.slice(5) : spec
|
|
245
|
+
if (/\.(?:tgz|tar\.gz)$/i.test(localPath) && await exists(resolve(localPath))) {
|
|
246
|
+
const archive = resolve(localPath)
|
|
247
|
+
const pinned = await pinArchive(home, archive)
|
|
248
|
+
installSpec = pinned.path
|
|
249
|
+
operation.install_spec = installSpec
|
|
250
|
+
operation.package_sha256 = pinned.sha256
|
|
251
|
+
await save(join(root, 'operation.json'), operation)
|
|
252
|
+
}
|
|
253
|
+
await (options.run || runInstall)(home, stagingName, installSpec, log)
|
|
146
254
|
const installed = JSON.parse(await readFile(join(staging, 'node_modules', 'dsh-openwrite', 'package.json'), 'utf8'))
|
|
147
255
|
if (installed.name !== 'dsh-openwrite' || !installed.version || !installed.dsh?.bundle?.patch) throw new Error('BUNDLE_INCOMPLETE: staged OpenWrite manifest is invalid')
|
|
148
256
|
const bundleRoot = join(staging, 'node_modules', 'dsh-openwrite')
|
|
@@ -152,7 +260,7 @@ export async function upgradeProfile(home, profile, spec, options = {}) {
|
|
|
152
260
|
operation.new_manifest_sha256 = digest(await readFile(join(staging, 'package.json')))
|
|
153
261
|
if (digest(await readFile(join(target, 'package.json'))) !== operation.old_manifest_sha256) throw new Error('PROFILE_CHANGED: the active profile changed during staging')
|
|
154
262
|
await stopped(home, profile)
|
|
155
|
-
await relocateProfileLinks(staging, staging, target)
|
|
263
|
+
await relocateProfileLinks(staging, staging, target, yaml)
|
|
156
264
|
operation.status = 'swapping'
|
|
157
265
|
await save(join(root, 'operation.json'), operation)
|
|
158
266
|
await rename(target, previous)
|
|
@@ -172,7 +280,16 @@ export async function upgradeProfile(home, profile, spec, options = {}) {
|
|
|
172
280
|
await save(join(root, 'operation.json'), operation)
|
|
173
281
|
error.message += `; operation=${id}; log=${logPath}`
|
|
174
282
|
throw error
|
|
175
|
-
} finally {
|
|
283
|
+
} finally {
|
|
284
|
+
try { await log?.close() } finally {
|
|
285
|
+
// The staging directory is a full profile copy including node_modules, so
|
|
286
|
+
// every failed upgrade used to leave hundreds of MB behind with nothing to
|
|
287
|
+
// reclaim it. On success it has been renamed onto the target, so removing
|
|
288
|
+
// it here is a no-op; on any failure path it is reclaimed.
|
|
289
|
+
try { await rm(staging, { recursive: true, force: true }) } catch { /* best effort */ }
|
|
290
|
+
await release()
|
|
291
|
+
}
|
|
292
|
+
}
|
|
176
293
|
}
|
|
177
294
|
|
|
178
295
|
/** Disable only an incomplete OpenWrite bundle so a stopped profile can boot for repair. */
|
|
@@ -187,6 +304,7 @@ export async function repairProfile(home, profile, apply = false, options = {})
|
|
|
187
304
|
const enabled = manifest.dsh?.profile?.bundles?.includes('dsh-openwrite') === true
|
|
188
305
|
if (!apply || !broken || !enabled) return { profile, incomplete: broken, enabled, changed: false }
|
|
189
306
|
await (options.assertStopped || assertHostStopped)(home, profile)
|
|
307
|
+
const { acquireLock } = upgradeDependencies(home, profile)
|
|
190
308
|
const release = await acquireLock(join(home, 'profiles'), { realpath: true, retries: 0 })
|
|
191
309
|
try {
|
|
192
310
|
if (await readFile(manifestPath, 'utf8') !== original) throw new Error('PROFILE_CHANGED: refresh repair inspection')
|
|
@@ -202,7 +320,17 @@ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1]
|
|
|
202
320
|
const value = name => { const i = process.argv.indexOf(name); return i < 0 ? undefined : process.argv[i + 1] }
|
|
203
321
|
const home = resolve(process.env.DSH_HOME || join(homedir(), '.dsh'))
|
|
204
322
|
try {
|
|
205
|
-
|
|
323
|
+
// `--profile` as the last argument yields undefined, and `|| 'web'` then
|
|
324
|
+
// silently operates on `web` and exits 0 - the same bug fixed in
|
|
325
|
+
// maintenance.mjs. A trailing flag is a usage error, not a default.
|
|
326
|
+
const profileIndex = process.argv.indexOf('--profile')
|
|
327
|
+
if (profileIndex >= 0) {
|
|
328
|
+
const named = process.argv[profileIndex + 1]
|
|
329
|
+
if (named === undefined || named === '') {
|
|
330
|
+
throw new Error('Usage: openwrite-safe-upgrade [--upgrade|--repair|--recover <id>] --profile <name> [--apply] [--package <path>]')
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const profile = profileIndex < 0 ? 'web' : process.argv[profileIndex + 1]
|
|
206
334
|
const result = value('--recover') ? await recoverUpgrade(home, value('--recover'))
|
|
207
335
|
: process.argv.includes('--repair') ? await repairProfile(home, profile, process.argv.includes('--apply'))
|
|
208
336
|
: await upgradeProfile(home, profile, value('--package'))
|
package/dog-tools.mjs
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export const name = '@dsh-novel/openwrite-dog-tools'
|
|
2
|
-
export const inject = ['tools', 'openwriteDog']
|
|
3
|
-
export function apply(ctx) {
|
|
4
|
-
for (const tool of ctx.openwriteDog.tools) ctx.effect(() => ctx.tools.register(tool))
|
|
5
|
-
ctx.effect(() => ctx.tools.guard(execution => {
|
|
6
|
-
if (!execution.name.startsWith('dog_') || ['dog_status', 'dog_wait', 'dog_validate'].includes(execution.name)) return
|
|
7
|
-
if (!execution.agent) return 'DoG requires an Agent session'
|
|
8
|
-
if (execution.agent.ctx.get('planMode')?.get(execution.agent).active) return 'DoG execution is disabled in plan mode'
|
|
9
|
-
if (!ctx.get('sandboxPolicy') || ctx.get('sandboxPolicy').resolve({ session: execution.agent.session }).mode === 'read-only') return 'DoG execution is disabled in read-only mode'
|
|
10
|
-
}))
|
|
11
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: dog-delivery-query
|
|
3
|
-
description: Query a chapter delivery DoG graph across manuscript, review, revision application, and re-review closure. Use after writing or reviewing a chapter, after revision actions, or when the user asks whether a chapter is truly ready to deliver.
|
|
4
|
-
user-invocable: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Language: follow the author's current conversation language for responses and help; preserve the manuscript language. Translate these instructions when presenting them.
|
|
8
|
-
|
|
9
|
-
# 章节交付 DoG 查询
|
|
10
|
-
|
|
11
|
-
章节写作、评审和修订工具会生成:
|
|
12
|
-
|
|
13
|
-
`data/novels/{id}/data/dog/deliveries/{chapter}/dog-graph.json`
|
|
14
|
-
|
|
15
|
-
这张图查询六个阶段:`writing → review → revision → application → rereview → closure`。
|
|
16
|
-
它不替代 OpenWrite 的写章、六域累加评审或修订服务。
|
|
17
|
-
|
|
18
|
-
## 查询流程
|
|
19
|
-
|
|
20
|
-
1. 从最近一次 `novel_write_chapter`、`novel_review_chapter`、`novel_revision_*`
|
|
21
|
-
或 `novel_task_get` 结果的 `dog_delivery.graphPath` 找到图;conductor 输出使用
|
|
22
|
-
`dog_delivery_graph_path`。
|
|
23
|
-
2. 依次调用 `dog_validate`、`dog_create`、`dog_run`、`dog_wait`,等待终态。
|
|
24
|
-
3. 用 `dog_status` 读取 `review`、`revision` 和 `closure` evidence,再决定领域动作。
|
|
25
|
-
4. 修订应用后必须重新调用 `novel_review_chapter`;只有复评对应当前正文 SHA 且门禁通过,
|
|
26
|
-
`closure` 才能成为 `closed`。
|
|
27
|
-
|
|
28
|
-
## 状态解释
|
|
29
|
-
|
|
30
|
-
- `review.missing`:正文存在但尚未评审。
|
|
31
|
-
- `closure.review_failed`:当前正文已评审但未通过门禁,进入修订闭环。
|
|
32
|
-
- `revision.proposal_pending`:提案已生成,等待作者确认应用或驳回。
|
|
33
|
-
- `revision.applied_requires_rereview`:修订已写入正文,但不能视为问题已解决。
|
|
34
|
-
- `closure.rereview_required`:必须对修订后的正文重新跑六域评审。
|
|
35
|
-
- `closure.closed`:当前正文的最新评审通过,可以交付。
|
|
36
|
-
|
|
37
|
-
## 约束
|
|
38
|
-
|
|
39
|
-
- DoG 只查询 canonical manuscript/review/revision 文件,不直接修改它们。
|
|
40
|
-
- 不因提案状态为 `applied` 就宣称问题关闭;必须以当前正文 SHA 对应的复评为准。
|
|
41
|
-
- `needs_human` 是等待评审、确认或复评,不得描述成成功。
|
|
42
|
-
- 应用或驳回修订仍遵守 OpenWrite 创作 Agent 的用户确认边界。
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: dog-import-query
|
|
3
|
-
description: Query a smart-import DoG graph after a novel has been split into chapters. Use to inspect import completeness, AI precheck errors, and pending outline/asset/canonical setup.
|
|
4
|
-
user-invocable: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Language: follow the author's current conversation language for responses and help; preserve the manuscript language. Translate these instructions when presenting them.
|
|
8
|
-
|
|
9
|
-
# 拆书导入 DoG 验收
|
|
10
|
-
|
|
11
|
-
`conductor/smart_import.py` 完成导入后会生成 `dog-graph.json`。这张图只验收导入
|
|
12
|
-
结果,不负责写入章节、资产或大纲。
|
|
13
|
-
|
|
14
|
-
## 工作流
|
|
15
|
-
|
|
16
|
-
1. 从导入输出中找到 `data/novels/{id}/data/dog/imports/{IMPORT_ID}/dog-graph.json`。
|
|
17
|
-
2. 依次调用 `dog_validate`、`dog_create`、`dog_run`、`dog_wait`,直到终态。
|
|
18
|
-
3. 用 `dog_status` 查看 `manifest` 和每个 `chapter-ch_*` 节点的 evidence。
|
|
19
|
-
4. 根节点通过后,读取 manifest 的 `construction.nextActions`,再调用规划和资产工具建立大纲、角色、世界观、进度和正典事件。
|
|
20
|
-
5. 资产/大纲建立完成后重新运行 `novel_review_chapter`,再用 `dog-review-query`
|
|
21
|
-
查询六域评审和 37 个 legacy leaf。
|
|
22
|
-
|
|
23
|
-
## 约束
|
|
24
|
-
|
|
25
|
-
- DoG 失败只说明导入文件缺失、为空或 manifest 不一致;不要直接修改正文。
|
|
26
|
-
- `status=partial|failed` 表示导入中途出错;先读取 manifest 的 `error` 和已完成的章节,再修复或重试导入。
|
|
27
|
-
- `aiCheck.status=failed` 要如实报告,不能当成“无问题”。
|
|
28
|
-
- `construction` 中的 `pending` 是后续建模待办,不是导入失败。
|
|
29
|
-
- 不要让 DoG verifier 再调用 Studio 导入接口或重新读取原始整本小说。
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: dog-review-query
|
|
3
|
-
description: Query an existing hierarchical OpenWrite review-v2 artifact through dsh-dog. Use after novel_review_chapter or when inspecting six quality domains, evidence, coverage, blockers, delivery status, or the 37 legacy checks. Never rerun model review from DoG.
|
|
4
|
-
user-invocable: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Language: follow the author's current conversation language for responses and help; preserve the manuscript language. Translate these instructions when presenting them.
|
|
8
|
-
|
|
9
|
-
# 分层评审 DoG 查询
|
|
10
|
-
|
|
11
|
-
这个 Skill 只查询 OpenWrite 已物化的 review-v2 artifact。OpenWrite 的
|
|
12
|
-
`novel_review_chapter` / conductor 是评审真源;DoG 读取 47 节点分层图:上下文、
|
|
13
|
-
六个质量域、硬门禁、确定性聚合和 37 个兼容查询 leaf。DoG 不调用评审模型。
|
|
14
|
-
|
|
15
|
-
## 工作流
|
|
16
|
-
|
|
17
|
-
1. 如果本轮还没有评审结果,先按 OpenWrite 写作流程调用 `novel_review_chapter`。
|
|
18
|
-
2. 找到评审输出目录中的 `dog-graph.json`。graph 与 `review.json`、`context.json`、
|
|
19
|
-
`domain-*.json`、`gate.json`、`aggregate.json` 和 `dim_01.json` …
|
|
20
|
-
`dim_37.json` 在同一目录。
|
|
21
|
-
3. 读取 graph 后依次调用 `dog_validate`、`dog_create`、`dog_run`、`dog_wait`。
|
|
22
|
-
所有节点均使用 programmatic `review-record` verifier;`dog_run` 后必须等到终态。
|
|
23
|
-
4. 用 `dog_status` 分别汇报 `qualityScore`、`coverage`、`gateStatus` 和
|
|
24
|
-
`deliveryStatus`,再按需展开域、criterion、evidence、issue 和 legacy ID。
|
|
25
|
-
`inconclusive` 表示覆盖不足、未请求或 artifact 不可判定,不能描述为通过。
|
|
26
|
-
5. 只有 OpenWrite 的审查问题才能进入 `novel_revisions_*` 回炉闭环。应用修订后
|
|
27
|
-
必须复评,再用 `dog-delivery-query` 确认 `closure.closed`。DoG 的 `failure`
|
|
28
|
-
只负责定位问题,不直接改正文或资产。
|
|
29
|
-
|
|
30
|
-
## 状态约定
|
|
31
|
-
|
|
32
|
-
- `qualityScore` 是有可定位正文证据的正向累加分;issue 不直接扣分。
|
|
33
|
-
- `coverage` 单独表达实际完成的评审范围;`inconclusive` 会降低覆盖率。
|
|
34
|
-
- `gateStatus=blocked` 来自 hard-gate 或 critical finding,不改写质量分。
|
|
35
|
-
- `deliveryStatus` 综合质量、覆盖、门禁和正文 SHA 新鲜度;它不是 `passed` 的别名。
|
|
36
|
-
- 37 个 leaf 是 `legacy_check_ids` 查询入口,不是 37 个顶层计分维度。
|
|
37
|
-
|
|
38
|
-
## 边界
|
|
39
|
-
|
|
40
|
-
- 不要用 DoG verifier 直接调用 Studio 写入接口。
|
|
41
|
-
- 不要把 warning 自动升级成失败;先展示 evidence,由 `review_gate` 和作者决定。
|
|
42
|
-
- 不要用 `dog_run` 生成、补写或刷新评审;artifact 缺失或 stale 时回到 OpenWrite 流程。
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// dsh-dog verifier for one materialized chapter-delivery stage.
|
|
3
|
-
|
|
4
|
-
import { readFile } from 'node:fs/promises'
|
|
5
|
-
|
|
6
|
-
const inputPath = process.argv[2]
|
|
7
|
-
if (!inputPath) {
|
|
8
|
-
process.stdout.write(JSON.stringify({ verdict: 'inconclusive', evidence: { error: 'missing input path' } }))
|
|
9
|
-
process.exit(0)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const value = JSON.parse(await readFile(inputPath, 'utf8'))
|
|
14
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error('delivery stage must be an object')
|
|
15
|
-
if (!['delivery-stage', 'chapter-delivery'].includes(value.recordType)) throw new Error('record is not a delivery artifact')
|
|
16
|
-
if (!['pass', 'fail', 'inconclusive'].includes(value.verdict)) throw new Error('delivery stage has an invalid verdict')
|
|
17
|
-
process.stdout.write(JSON.stringify({
|
|
18
|
-
verdict: value.verdict,
|
|
19
|
-
evidence: {
|
|
20
|
-
chapterId: value.chapterId,
|
|
21
|
-
stage: value.stage,
|
|
22
|
-
status: value.status ?? (value.readyForDelivery ? 'ready' : 'not_ready'),
|
|
23
|
-
readyForDelivery: value.readyForDelivery,
|
|
24
|
-
...(value.evidence !== null && typeof value.evidence === 'object' ? value.evidence : {}),
|
|
25
|
-
},
|
|
26
|
-
}))
|
|
27
|
-
} catch (error) {
|
|
28
|
-
process.stdout.write(JSON.stringify({
|
|
29
|
-
verdict: 'inconclusive',
|
|
30
|
-
evidence: { error: error instanceof Error ? error.message : String(error) },
|
|
31
|
-
}))
|
|
32
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// dsh-dog verifier for one imported manuscript chapter.
|
|
3
|
-
|
|
4
|
-
import { readFile } from 'node:fs/promises'
|
|
5
|
-
|
|
6
|
-
const inputPath = process.argv[2]
|
|
7
|
-
if (!inputPath) {
|
|
8
|
-
process.stdout.write(JSON.stringify({ verdict: 'inconclusive', evidence: { error: 'missing input path' } }))
|
|
9
|
-
process.exit(0)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const text = await readFile(inputPath, 'utf8')
|
|
14
|
-
if (!text.trim()) throw new Error('chapter file is empty')
|
|
15
|
-
const heading = text.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? ''
|
|
16
|
-
if (!heading) throw new Error('chapter file has no markdown title')
|
|
17
|
-
process.stdout.write(JSON.stringify({ verdict: 'pass', evidence: { heading, bytes: Buffer.byteLength(text) } }))
|
|
18
|
-
} catch (error) {
|
|
19
|
-
process.stdout.write(JSON.stringify({ verdict: 'fail', evidence: { error: error instanceof Error ? error.message : String(error) } }))
|
|
20
|
-
}
|