@aiwg/cli 2026.9.4 → 2026.9.6
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/THIRD_PARTY_NOTICES.md +16 -0
- package/agentic/code/providers/capability-matrix.yaml +45 -3
- package/agentic/code/providers/deepseek-harness/README.md +8 -0
- package/agentic/code/providers/deepseek-harness/aiwg.cordis.patch.yml +10 -0
- package/agentic/code/providers/model-capabilities.v1.json +11 -0
- package/agentic/code/providers/model-catalog.v1.json +8 -0
- package/bin/aiwg.mjs +1 -0
- package/dist/src/api/index.d.ts +14 -0
- package/dist/src/api/index.js +14 -0
- package/dist/src/artifacts/corpus-tools/source-types.js +1 -0
- package/dist/src/artifacts/index-files.js +17 -2
- package/dist/src/artifacts/repair.js +55 -6
- package/dist/src/catalog/cli.js +21 -7
- package/dist/src/catalog/cli.mjs +22 -7
- package/dist/src/cli/agent-spawn.js +10 -1
- package/dist/src/cli/handlers/artifacts.js +22 -3
- package/dist/src/cli/handlers/help.js +3 -0
- package/dist/src/cli/handlers/index.js +5 -1
- package/dist/src/cli/handlers/models.js +2 -2
- package/dist/src/cli/handlers/output-mode.js +1 -1
- package/dist/src/cli/handlers/runtime-info.js +1 -1
- package/dist/src/cli/handlers/sessions.js +55 -15
- package/dist/src/cli/handlers/steward.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +5 -0
- package/dist/src/cli/handlers/writer-profile.js +110 -0
- package/dist/src/cli/handlers/writing.js +122 -0
- package/dist/src/cli/router.js +5 -1
- package/dist/src/config/project-artifacts-runtime.mjs +33 -1
- package/dist/src/config/project-artifacts.js +1 -1
- package/dist/src/dataset/fortemi-dataset-execution.d.ts +23 -0
- package/dist/src/dataset/fortemi-dataset-execution.js +158 -0
- package/dist/src/dataset/fortemi-live-qualification.d.ts +4 -2
- package/dist/src/dataset/fortemi-live-qualification.js +20 -21
- package/dist/src/dataset/fortemi-run-receipt.d.ts +44 -0
- package/dist/src/dataset/fortemi-run-receipt.js +74 -0
- package/dist/src/dataset/index.d.ts +2 -0
- package/dist/src/dataset/index.js +2 -0
- package/dist/src/extensions/commands/definitions.js +24 -0
- package/dist/src/extensions/manifest.js +3 -0
- package/dist/src/mcp/server.mjs +2 -0
- package/dist/src/mcp/tools/writer-profiles.mjs +40 -0
- package/dist/src/models/model-capabilities.v1.json +11 -0
- package/dist/src/models/model-catalog.v1.json +8 -0
- package/dist/src/models/provider-policy.js +1 -1
- package/dist/src/network-analysis/analyzer.js +667 -0
- package/dist/src/network-analysis/citations.js +107 -0
- package/dist/src/network-analysis/forensics.js +132 -0
- package/dist/src/network-analysis/governance.js +216 -0
- package/dist/src/network-analysis/index.js +10 -0
- package/dist/src/network-analysis/probe.js +405 -0
- package/dist/src/network-analysis/recipes.js +88 -0
- package/dist/src/network-analysis/research.js +181 -0
- package/dist/src/network-analysis/termshark.js +252 -0
- package/dist/src/network-analysis/verification.js +171 -0
- package/dist/src/output-modes/registry.js +37 -6
- package/dist/src/output-modes/runtime.js +164 -28
- package/dist/src/providers/capability-matrix.yaml +45 -3
- package/dist/src/providers/provider-definitions.js +49 -0
- package/dist/src/providers/provider-inventory.js +1 -0
- package/dist/src/providers/transformation-receipt.js +3 -2
- package/dist/src/sessions/adapters/deepseek-harness.js +178 -0
- package/dist/src/sessions/batch-import.js +7 -0
- package/dist/src/sessions/contracts.js +2 -1
- package/dist/src/sessions/index.js +1 -0
- package/dist/src/sessions/workspace-discovery.js +5 -1
- package/dist/src/skills/deployer.js +6 -6
- package/dist/src/smiths/context-pipeline/workspace-context.js +7 -0
- package/dist/src/writing/channel-packs.js +13 -0
- package/dist/src/writing/contextual-diagnostics.js +142 -0
- package/dist/src/writing/example-generator.js +7 -6
- package/dist/src/writing/exemplar-selection.js +186 -0
- package/dist/src/writing/fidelity.js +61 -0
- package/dist/src/writing/validation-engine.js +32 -15
- package/dist/src/writing/voice-evaluation.js +301 -0
- package/dist/src/writing/voice-revision.js +201 -0
- package/dist/src/writing/writer-migration.js +216 -0
- package/dist/src/writing/writer-profile-legacy.js +145 -0
- package/dist/src/writing/writer-profile-store.js +117 -0
- package/dist/src/writing/writer-profile.js +222 -0
- package/dist/src/writing/writing-brief.js +166 -0
- package/dist/src/writing/writing-channels.js +63 -0
- package/dist/src/writing/writing-consumer.js +39 -0
- package/dist/src/writing/writing-receipt.js +266 -0
- package/package.json +2 -1
- package/schemas/dataset/fortemi-live-qualification-receipt.v2.schema.json +196 -0
- package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/authority.json +12 -0
- package/schemas/dataset/fortemi-run-receipt/validation-1.0.1/run-receipt.schema.json +819 -0
- package/tools/agents/deploy-agents.mjs +7 -3
- package/tools/agents/providers/antigravity.mjs +1 -1
- package/tools/agents/providers/base.mjs +3 -2
- package/tools/agents/providers/deepseek-harness.mjs +66 -0
- package/tools/agents/providers/hermes.mjs +1 -1
- package/tools/agents/providers/openhuman.mjs +2 -2
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, rename, rm, readdir, lstat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
|
|
5
|
+
import { parseWriterProfile } from './writer-profile.js';
|
|
6
|
+
/** Explicit scopes never fall through to a profile in another scope. */
|
|
7
|
+
export class WriterProfileStore {
|
|
8
|
+
directory;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
if (options.scope && !['project', 'user'].includes(options.scope))
|
|
11
|
+
throw new Error('Invalid writer profile scope');
|
|
12
|
+
this.directory = path.join(options.scope === 'user'
|
|
13
|
+
? resolveUserConfigDir({ configDir: options.userConfigDir })
|
|
14
|
+
: path.resolve(options.cwd, '.aiwg'), 'writer-profiles');
|
|
15
|
+
}
|
|
16
|
+
profilePath(id) {
|
|
17
|
+
if (!/^[a-z0-9][a-z0-9.-]{0,79}$/.test(id) || id.includes('..'))
|
|
18
|
+
throw new Error('Invalid writer profile ID');
|
|
19
|
+
return path.join(this.directory, `${id}.json`);
|
|
20
|
+
}
|
|
21
|
+
managedMigrationBackupDirectory(id) {
|
|
22
|
+
this.profilePath(id);
|
|
23
|
+
return path.join(this.directory, 'migration-backups', id);
|
|
24
|
+
}
|
|
25
|
+
async regularFile(file) {
|
|
26
|
+
const stat = await lstat(file);
|
|
27
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
28
|
+
throw new Error('Writer profile must be a regular file');
|
|
29
|
+
}
|
|
30
|
+
async read(id) {
|
|
31
|
+
const file = this.profilePath(id);
|
|
32
|
+
await this.regularFile(file);
|
|
33
|
+
const profile = parseWriterProfile(JSON.parse(await readFile(file, 'utf8')));
|
|
34
|
+
if (profile.id !== id)
|
|
35
|
+
throw new Error('Writer profile ID mismatch');
|
|
36
|
+
return profile;
|
|
37
|
+
}
|
|
38
|
+
async list() {
|
|
39
|
+
try {
|
|
40
|
+
const files = await readdir(this.directory, { withFileTypes: true });
|
|
41
|
+
return files.filter(file => file.isFile() && /^[a-z0-9][a-z0-9.-]*\.json$/.test(file.name))
|
|
42
|
+
.map(file => file.name.slice(0, -5)).sort();
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code === 'ENOENT')
|
|
46
|
+
return [];
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async locked(id, action) {
|
|
51
|
+
const file = this.profilePath(id);
|
|
52
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
53
|
+
const lock = `${file}.lock`;
|
|
54
|
+
try {
|
|
55
|
+
await mkdir(lock, { mode: 0o700 });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error.code === 'EEXIST')
|
|
59
|
+
throw new Error('Writer profile is locked by another operation');
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return await action();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await rm(lock, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** expectedRevision=0 creates; updates require the current revision. */
|
|
70
|
+
async save(input, expectedRevision, options = {}) {
|
|
71
|
+
const profile = parseWriterProfile(input);
|
|
72
|
+
return this.locked(profile.id, async () => {
|
|
73
|
+
let current;
|
|
74
|
+
try {
|
|
75
|
+
current = await this.read(profile.id);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code !== 'ENOENT')
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
if ((current?.revision ?? 0) !== expectedRevision)
|
|
82
|
+
throw new Error('Writer profile revision conflict');
|
|
83
|
+
const next = parseWriterProfile({ ...profile, revision: expectedRevision + 1 });
|
|
84
|
+
const file = this.profilePath(profile.id);
|
|
85
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
86
|
+
try {
|
|
87
|
+
await writeFile(temporary, JSON.stringify(next, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
|
|
88
|
+
// Invalidate before publishing: an interrupted update may lose cache, never retain stale text.
|
|
89
|
+
await this.invalidate(profile.id, options);
|
|
90
|
+
await rename(temporary, file);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await rm(temporary, { force: true });
|
|
94
|
+
}
|
|
95
|
+
return next;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async invalidate(id, options = {}) {
|
|
99
|
+
this.profilePath(id);
|
|
100
|
+
// This store deliberately retains no historical sample content.
|
|
101
|
+
for (const kind of ['cache', 'history']) {
|
|
102
|
+
await rm(path.join(this.directory, kind, id), { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
if (!options.preserveMigrationBackups)
|
|
105
|
+
await rm(this.managedMigrationBackupDirectory(id), { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
async delete(id, expectedRevision) {
|
|
108
|
+
await this.locked(id, async () => {
|
|
109
|
+
const current = await this.read(id);
|
|
110
|
+
if (current.revision !== expectedRevision)
|
|
111
|
+
throw new Error('Writer profile revision conflict');
|
|
112
|
+
await this.invalidate(id);
|
|
113
|
+
await rm(this.profilePath(id));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=writer-profile-store.js.map
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { redactText } from '../governance/redaction.js';
|
|
4
|
+
import { validateLegacyWriterProfile } from './writer-profile-legacy.js';
|
|
5
|
+
const hash = (text) => createHash('sha256').update(text).digest('hex');
|
|
6
|
+
const id = z.string().min(1).max(120).regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/);
|
|
7
|
+
const provenance = z.object({ source: z.string().min(1).max(2000), license: z.string().min(1).max(200) }).strict();
|
|
8
|
+
const evidence = z.object({ sampleId: id, start: z.number().int().nonnegative(), end: z.number().int().positive() }).strict();
|
|
9
|
+
/** Closed expression controls exclude identity, personality and generated signature phrases. */
|
|
10
|
+
export const writerPreferenceValues = {
|
|
11
|
+
dialect: ['unspecified', 'en-US', 'en-GB', 'en-AU', 'en-CA', 'fr-FR', 'fr-CA'],
|
|
12
|
+
register: ['conversational', 'professional', 'technical', 'formal'],
|
|
13
|
+
formality: ['casual', 'neutral', 'formal'],
|
|
14
|
+
warmth: ['reserved', 'neutral', 'warm'],
|
|
15
|
+
directness: ['gentle', 'neutral', 'direct'],
|
|
16
|
+
sentenceLength: ['short', 'varied', 'long'],
|
|
17
|
+
paragraphLength: ['short', 'varied', 'long'],
|
|
18
|
+
contractions: ['prefer', 'avoid', 'contextual'],
|
|
19
|
+
punctuation: ['minimal', 'varied', 'contextual'],
|
|
20
|
+
lexicalChoice: ['familiar', 'technical', 'contextual'],
|
|
21
|
+
rhetoric: ['plain', 'illustrative', 'contextual'],
|
|
22
|
+
structure: ['prose', 'lists', 'contextual'],
|
|
23
|
+
};
|
|
24
|
+
const preferenceKey = z.enum(Object.keys(writerPreferenceValues));
|
|
25
|
+
const preference = z.object({
|
|
26
|
+
id, key: preferenceKey, value: z.string(), origin: z.enum(['explicit', 'inferred']),
|
|
27
|
+
confidence: z.enum(['low', 'moderate', 'high']), evidence: z.array(evidence).default([]),
|
|
28
|
+
task: z.string().max(120).optional(), status: z.enum(['accepted', 'rejected']).default('accepted'),
|
|
29
|
+
}).strict();
|
|
30
|
+
const override = z.object({ key: preferenceKey, action: z.enum(['set', 'reject', 'reset']), value: z.string().optional(), task: z.string().max(120).optional() }).strict();
|
|
31
|
+
const sample = z.object({
|
|
32
|
+
id, text: z.string().max(1_000_000).optional(), sha256: z.string().regex(/^[a-f0-9]{64}$/), approved: z.boolean(),
|
|
33
|
+
status: z.enum(['active', 'revoked']), provenance,
|
|
34
|
+
rights: z.object({ useForVoice: z.boolean(), shareText: z.boolean() }).strict(),
|
|
35
|
+
sensitivity: z.enum(['public', 'private', 'secret']),
|
|
36
|
+
}).strict();
|
|
37
|
+
export const legacyProfileAttachmentSchema = z.object({
|
|
38
|
+
format: z.enum(['yaml', 'json']),
|
|
39
|
+
kind: z.enum(['addon-template', 'python-generated', 'python-analyzed', 'python-blended', 'ts-analyzer', 'ts-calibration']),
|
|
40
|
+
raw: z.string(), sha256: z.string().regex(/^[a-f0-9]{64}$/), payload: z.unknown(),
|
|
41
|
+
}).strict();
|
|
42
|
+
export const writerProfileSchema = z.object({
|
|
43
|
+
schemaVersion: z.literal(1), revision: z.number().int().positive().default(1), metadataSharingApproved: z.boolean().default(false), id: z.string().regex(/^[a-z0-9][a-z0-9.-]{0,79}$/).refine(v => !v.includes('..')), version: z.string().regex(/^\d+\.\d+\.\d+$/), name: z.string().min(1).max(200), provenance,
|
|
44
|
+
samples: z.array(sample), preferences: z.array(preference), overrides: z.array(override).default([]),
|
|
45
|
+
counterexamples: z.array(evidence).default([]), legacy: legacyProfileAttachmentSchema.optional(),
|
|
46
|
+
cacheEpoch: z.number().int().nonnegative().default(0),
|
|
47
|
+
}).strict().superRefine((p, ctx) => {
|
|
48
|
+
const problem = (message) => ctx.addIssue({ code: z.ZodIssueCode.custom, message });
|
|
49
|
+
if (new Set(p.samples.map(s => s.id)).size !== p.samples.length)
|
|
50
|
+
problem('Duplicate sample IDs');
|
|
51
|
+
if (new Set(p.preferences.map(s => s.id)).size !== p.preferences.length)
|
|
52
|
+
problem('Duplicate preference IDs');
|
|
53
|
+
const samples = new Map(p.samples.map(s => [s.id, s]));
|
|
54
|
+
for (const s of p.samples) {
|
|
55
|
+
if (s.text !== undefined && hash(s.text) !== s.sha256)
|
|
56
|
+
problem('Sample integrity mismatch');
|
|
57
|
+
if (s.status === 'revoked' && s.text !== undefined)
|
|
58
|
+
problem('Revoked sample must not retain text');
|
|
59
|
+
if (s.sensitivity === 'secret' && s.rights.shareText)
|
|
60
|
+
problem('Secret samples cannot permit sharing');
|
|
61
|
+
}
|
|
62
|
+
const checkEvidence = (e) => {
|
|
63
|
+
const s = samples.get(e.sampleId);
|
|
64
|
+
if (!s || !s.approved || !s.rights.useForVoice || s.status !== 'active' || s.sensitivity === 'secret' || s.text === undefined || e.start >= e.end || e.end > s.text.length)
|
|
65
|
+
problem('Evidence must reference an approved usable sample span');
|
|
66
|
+
if (s?.text) {
|
|
67
|
+
// Do not allow a span boundary to split a surrogate pair.
|
|
68
|
+
for (const offset of [e.start, e.end])
|
|
69
|
+
if (offset > 0 && offset < s.text.length && /[\uD800-\uDBFF]/.test(s.text[offset - 1]) && /[\uDC00-\uDFFF]/.test(s.text[offset]))
|
|
70
|
+
problem('Evidence splits a Unicode code point');
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
for (const pref of p.preferences) {
|
|
74
|
+
if (!writerPreferenceValues[pref.key].includes(pref.value))
|
|
75
|
+
problem('Unsupported expression value');
|
|
76
|
+
if (pref.origin === 'inferred' && !pref.evidence.length)
|
|
77
|
+
problem('Inferred preference requires evidence');
|
|
78
|
+
pref.evidence.forEach(checkEvidence);
|
|
79
|
+
}
|
|
80
|
+
for (const o of p.overrides) {
|
|
81
|
+
if (o.action === 'set' && (!o.value || !writerPreferenceValues[o.key].includes(o.value)))
|
|
82
|
+
problem('Set override requires a supported value');
|
|
83
|
+
if (o.action !== 'set' && o.value !== undefined)
|
|
84
|
+
problem('Only set overrides accept a value');
|
|
85
|
+
}
|
|
86
|
+
p.counterexamples.forEach(checkEvidence);
|
|
87
|
+
if (p.legacy) {
|
|
88
|
+
try {
|
|
89
|
+
validateLegacyWriterProfile(p.legacy);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
problem('Legacy attachment integrity mismatch');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
/** Errors never echo personal text, schema values or raw legacy payloads. */
|
|
97
|
+
export function parseWriterProfile(input) {
|
|
98
|
+
const parsed = writerProfileSchema.safeParse(input);
|
|
99
|
+
if (!parsed.success)
|
|
100
|
+
throw new Error('Invalid writer profile: schema, evidence, rights or integrity validation failed');
|
|
101
|
+
return parsed.data;
|
|
102
|
+
}
|
|
103
|
+
export function compileWriterProfile(input, options = {}) {
|
|
104
|
+
const p = parseWriterProfile(input);
|
|
105
|
+
const settings = {};
|
|
106
|
+
const diagnostics = [];
|
|
107
|
+
const scopeMatches = (task) => task === undefined || task === options.task;
|
|
108
|
+
for (const key of Object.keys(writerPreferenceValues)) {
|
|
109
|
+
let candidates = p.preferences.filter(v => v.key === key && v.status === 'accepted' && scopeMatches(v.task));
|
|
110
|
+
const specific = candidates.filter(v => v.task !== undefined);
|
|
111
|
+
if (specific.length)
|
|
112
|
+
candidates = specific;
|
|
113
|
+
const explicit = candidates.filter(v => v.origin === 'explicit');
|
|
114
|
+
candidates = explicit.length ? explicit : candidates.filter(v => v.confidence !== 'low');
|
|
115
|
+
const values = new Set(candidates.map(v => v.value));
|
|
116
|
+
if (values.size === 1)
|
|
117
|
+
settings[key] = candidates[0].value;
|
|
118
|
+
else if (values.size > 1)
|
|
119
|
+
diagnostics.push(`Conflicting ${key} evidence; generic fallback for this setting.`);
|
|
120
|
+
const applicable = p.overrides.filter(o => o.key === key && scopeMatches(o.task));
|
|
121
|
+
const taskOverrides = applicable.filter(o => o.task !== undefined);
|
|
122
|
+
const last = (taskOverrides.length ? taskOverrides : applicable).at(-1);
|
|
123
|
+
if (last?.action === 'set')
|
|
124
|
+
settings[key] = last.value;
|
|
125
|
+
else if (last?.action === 'reject')
|
|
126
|
+
delete settings[key];
|
|
127
|
+
// Reset removes the override; retain the underlying resolved preference.
|
|
128
|
+
}
|
|
129
|
+
const fallback = Object.keys(settings).length === 0;
|
|
130
|
+
if (fallback)
|
|
131
|
+
diagnostics.push('No unambiguous supported preferences; explicit generic fallback.');
|
|
132
|
+
const instructions = [
|
|
133
|
+
'Preserve facts, citations, author intent, uncertainty and protected literals. Expression settings do not change evidence confidence.',
|
|
134
|
+
'Do not invent personal identity, experiences or signature phrases. Apply these advisory expression settings only where the task permits.',
|
|
135
|
+
fallback ? 'Use the existing generic writing behavior; no personal style has been established.' : `Expression settings: ${JSON.stringify(settings)}.`,
|
|
136
|
+
].join('\n');
|
|
137
|
+
return { profile: {
|
|
138
|
+
id: `writer-${p.id}`, version: p.version, description: 'Author-controlled advisory expression profile.',
|
|
139
|
+
kind: 'voice', stage: 'voice', order: 100, instructions,
|
|
140
|
+
provenance: { ...p.provenance }, validation: { level: 'advisory' },
|
|
141
|
+
protectedContent: ['code', 'commands', 'citations', 'quoted-text', 'identifiers', 'machine-readable-blocks'],
|
|
142
|
+
}, fallback, diagnostics };
|
|
143
|
+
}
|
|
144
|
+
function advance(p) {
|
|
145
|
+
const parts = p.version.split('.').map(Number);
|
|
146
|
+
parts[2]++;
|
|
147
|
+
p.version = parts.join('.');
|
|
148
|
+
p.cacheEpoch++;
|
|
149
|
+
}
|
|
150
|
+
/** Pure operation; stores use cacheEpoch to invalidate dependent retrieval caches. */
|
|
151
|
+
export function revokeWriterSample(input, sampleId) {
|
|
152
|
+
const p = parseWriterProfile(input);
|
|
153
|
+
const s = p.samples.find(v => v.id === sampleId);
|
|
154
|
+
if (!s)
|
|
155
|
+
throw new Error('Unknown writer sample');
|
|
156
|
+
if (s.status === 'revoked')
|
|
157
|
+
return p;
|
|
158
|
+
delete s.text;
|
|
159
|
+
s.status = 'revoked';
|
|
160
|
+
s.approved = false;
|
|
161
|
+
s.rights.useForVoice = false;
|
|
162
|
+
s.rights.shareText = false;
|
|
163
|
+
p.preferences = p.preferences.filter(v => v.origin === 'explicit' || !v.evidence.some(e => e.sampleId === sampleId));
|
|
164
|
+
for (const v of p.preferences)
|
|
165
|
+
v.evidence = v.evidence.filter(e => e.sampleId !== sampleId);
|
|
166
|
+
p.counterexamples = p.counterexamples.filter(e => e.sampleId !== sampleId);
|
|
167
|
+
// Legacy payloads may duplicate the revoked text; remove the opaque attachment.
|
|
168
|
+
delete p.legacy;
|
|
169
|
+
advance(p);
|
|
170
|
+
return parseWriterProfile(p);
|
|
171
|
+
}
|
|
172
|
+
/** Private export is lossless. Shared output requires public approval and passes known-secret redaction. */
|
|
173
|
+
export function exportWriterProfile(input, mode = 'shared') {
|
|
174
|
+
const p = parseWriterProfile(input);
|
|
175
|
+
if (mode === 'private')
|
|
176
|
+
return p;
|
|
177
|
+
const metadata = { id: p.id, name: p.name, provenance: p.provenance, samples: p.samples.map(s => ({ id: s.id, provenance: s.provenance })), preferences: p.preferences.map(v => ({ id: v.id, task: v.task })), overrides: p.overrides };
|
|
178
|
+
if (redactText(JSON.stringify(metadata)).sensitivity === 'sensitive')
|
|
179
|
+
p.metadataSharingApproved = false;
|
|
180
|
+
const removed = new Set();
|
|
181
|
+
for (const s of p.samples) {
|
|
182
|
+
const allowed = s.status === 'active' && s.approved && s.sensitivity === 'public' && s.rights.shareText && (s.text === undefined || redactText(s.text).sensitivity === 'none');
|
|
183
|
+
if (!allowed) {
|
|
184
|
+
delete s.text;
|
|
185
|
+
removed.add(s.id);
|
|
186
|
+
}
|
|
187
|
+
if (!p.metadataSharingApproved)
|
|
188
|
+
s.provenance.source = '[redacted]';
|
|
189
|
+
}
|
|
190
|
+
p.preferences = p.preferences.filter(v => v.origin === 'explicit' || !v.evidence.some(e => removed.has(e.sampleId)));
|
|
191
|
+
for (const v of p.preferences)
|
|
192
|
+
v.evidence = v.evidence.filter(e => !removed.has(e.sampleId));
|
|
193
|
+
p.counterexamples = p.counterexamples.filter(e => removed.has(e.sampleId) === false);
|
|
194
|
+
if (mode === 'shared') {
|
|
195
|
+
delete p.legacy;
|
|
196
|
+
if (!p.metadataSharingApproved) {
|
|
197
|
+
p.id = 'shared-profile';
|
|
198
|
+
p.name = 'Shared writer profile';
|
|
199
|
+
p.provenance = { source: '[redacted]', license: '[redacted]' };
|
|
200
|
+
const ids = new Map(p.samples.map((s, i) => [s.id, `sample-${i + 1}`]));
|
|
201
|
+
for (const s of p.samples) {
|
|
202
|
+
s.id = ids.get(s.id);
|
|
203
|
+
s.provenance = { source: '[redacted]', license: '[redacted]' };
|
|
204
|
+
}
|
|
205
|
+
p.preferences = p.preferences.filter(v => v.task === undefined);
|
|
206
|
+
p.overrides = p.overrides.filter(v => v.task === undefined);
|
|
207
|
+
p.preferences.forEach((v, i) => { v.id = `preference-${i + 1}`; v.evidence.forEach(e => { e.sampleId = ids.get(e.sampleId); }); });
|
|
208
|
+
p.counterexamples.forEach(e => { e.sampleId = ids.get(e.sampleId); });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return parseWriterProfile(p);
|
|
212
|
+
}
|
|
213
|
+
/** Metadata-only inspection suitable for CLI logs; never includes sample text or preference evidence text. */
|
|
214
|
+
export function inspectWriterProfile(input) {
|
|
215
|
+
const p = parseWriterProfile(input);
|
|
216
|
+
return { schemaVersion: p.schemaVersion, id: p.id, version: p.version, revision: p.revision, cacheEpoch: p.cacheEpoch,
|
|
217
|
+
sampleCount: p.samples.length, approvedSampleCount: p.samples.filter(s => s.approved && s.status === 'active').length,
|
|
218
|
+
preferenceCount: p.preferences.length, legacyFormat: p.legacy?.format ?? null,
|
|
219
|
+
fallback: compileWriterProfile(p).fallback,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=writer-profile.js.map
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const id = z.string().min(1).max(120).regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/);
|
|
4
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/);
|
|
5
|
+
const text = z.string().min(1);
|
|
6
|
+
const span = { start: z.number().int().nonnegative(), end: z.number().int().nonnegative() };
|
|
7
|
+
const evidence = z.object({ inputId: id, ...span }).strict();
|
|
8
|
+
const input = z.object({
|
|
9
|
+
id, kind: z.enum(['source', 'author-notes', 'existing-draft']), text,
|
|
10
|
+
sha256: digest, provenance: z.object({ source: text, version: text }).strict(),
|
|
11
|
+
authorApproved: z.boolean(),
|
|
12
|
+
}).strict();
|
|
13
|
+
const proposition = z.object({
|
|
14
|
+
id, text, evidenceStrength: z.enum(['verified', 'reported', 'preliminary', 'experimental', 'unverified']),
|
|
15
|
+
evidence: z.array(evidence).min(1), qualifiers: z.array(text),
|
|
16
|
+
}).strict();
|
|
17
|
+
const authorClaim = z.object({
|
|
18
|
+
id, kind: z.enum(['experience', 'opinion', 'intent', 'rationale']), text,
|
|
19
|
+
evidence: z.array(evidence).min(1),
|
|
20
|
+
}).strict();
|
|
21
|
+
const correction = z.object({
|
|
22
|
+
id, ...span, expected: z.string(), replacement: z.string(), reason: text,
|
|
23
|
+
authorAuthorized: z.literal(true),
|
|
24
|
+
}).strict();
|
|
25
|
+
export const writingBriefSchema = z.object({
|
|
26
|
+
schemaVersion: z.literal(1), id,
|
|
27
|
+
operation: z.enum(['draft-from-notes', 'edit-existing', 'proofread-only', 'continue-author-text']),
|
|
28
|
+
reader: z.object({ task: z.string(), audience: text, requirements: z.array(text) }).strict(),
|
|
29
|
+
intendedAction: z.string(), exclusions: z.array(text), inputs: z.array(input),
|
|
30
|
+
propositions: z.array(proposition), limitations: z.array(proposition), authorClaims: z.array(authorClaim),
|
|
31
|
+
sourceInputId: id.optional(),
|
|
32
|
+
permissions: z.object({ rephrase: z.boolean(), reorder: z.boolean(), addContent: z.boolean(), corrections: z.array(correction) }).strict(),
|
|
33
|
+
}).strict();
|
|
34
|
+
export const writingBriefHash = (value) => createHash('sha256').update(value, 'utf8').digest('hex');
|
|
35
|
+
function boundary(source, offset) {
|
|
36
|
+
return offset >= 0 && offset <= source.length
|
|
37
|
+
&& !(offset > 0 && offset < source.length && /[\uD800-\uDBFF]/.test(source[offset - 1]) && /[\uDC00-\uDFFF]/.test(source[offset]));
|
|
38
|
+
}
|
|
39
|
+
function overlaps(a, b) {
|
|
40
|
+
return a.start === a.end ? a.start >= b.start && a.start <= b.end : a.start < b.end && a.end > b.start;
|
|
41
|
+
}
|
|
42
|
+
/** Structural checks prove reference/permission integrity, not truth or semantic entailment. */
|
|
43
|
+
export function validateWritingBrief(value) {
|
|
44
|
+
const parsed = writingBriefSchema.safeParse(value);
|
|
45
|
+
if (!parsed.success)
|
|
46
|
+
return { valid: false, diagnostics: [{ code: 'schema', message: 'Invalid writing brief shape; input text is omitted.' }] };
|
|
47
|
+
const b = parsed.data;
|
|
48
|
+
const diagnostics = [];
|
|
49
|
+
const report = (code, message, referenceId) => diagnostics.push({ code, message, ...(referenceId ? { referenceId } : {}) });
|
|
50
|
+
if (!b.reader.task.trim())
|
|
51
|
+
report('editorial-gap', 'Supply the reader task before drafting.');
|
|
52
|
+
if (!b.intendedAction.trim())
|
|
53
|
+
report('editorial-gap', 'Supply the intended reader action before drafting.');
|
|
54
|
+
if (!b.propositions.length)
|
|
55
|
+
report('editorial-gap', 'Supply supported propositions or look up context before drafting.');
|
|
56
|
+
for (const group of [b.inputs, [...b.propositions, ...b.limitations, ...b.authorClaims], b.permissions.corrections]) {
|
|
57
|
+
if (new Set(group.map(v => v.id)).size !== group.length)
|
|
58
|
+
report('reference', 'Duplicate identifiers are ambiguous.');
|
|
59
|
+
}
|
|
60
|
+
const inputs = new Map(b.inputs.map(v => [v.id, v]));
|
|
61
|
+
for (const i of b.inputs)
|
|
62
|
+
if (writingBriefHash(i.text) !== i.sha256)
|
|
63
|
+
report('integrity', 'Input digest mismatch.', i.id);
|
|
64
|
+
for (const claim of [...b.propositions, ...b.limitations, ...b.authorClaims]) {
|
|
65
|
+
for (const e of claim.evidence) {
|
|
66
|
+
const source = inputs.get(e.inputId);
|
|
67
|
+
if (!source || e.start >= e.end || !boundary(source.text, e.start) || !boundary(source.text, e.end))
|
|
68
|
+
report('reference', 'Evidence requires an existing input and a nonempty Unicode-safe span.', claim.id);
|
|
69
|
+
if ('kind' in claim && (!source || source.kind !== 'author-notes' || !source.authorApproved))
|
|
70
|
+
report('unsupported-claim', 'Personal experience, opinion, intent and rationale require approved author notes.', claim.id);
|
|
71
|
+
}
|
|
72
|
+
if ('kind' in claim && !claim.evidence.some(e => { const i = inputs.get(e.inputId); return i?.text.slice(e.start, e.end).includes(claim.text); }))
|
|
73
|
+
report('unsupported-claim', 'Author claim wording must occur in the cited approved notes; paraphrases require review.', claim.id);
|
|
74
|
+
if ('qualifiers' in claim && claim.qualifiers.some(q => !claim.text.includes(q)))
|
|
75
|
+
report('protected-content', 'A declared qualifier is missing from its proposition.', claim.id);
|
|
76
|
+
if ('evidenceStrength' in claim && claim.evidenceStrength === 'experimental' && !claim.qualifiers.length)
|
|
77
|
+
report('protected-content', 'Experimental propositions require an explicit protected qualifier.', claim.id);
|
|
78
|
+
}
|
|
79
|
+
const source = b.sourceInputId ? inputs.get(b.sourceInputId) : undefined;
|
|
80
|
+
if (b.sourceInputId && !source)
|
|
81
|
+
report('reference', 'Selected draft input does not exist.');
|
|
82
|
+
if (b.operation !== 'draft-from-notes' && (!source || source.kind !== 'existing-draft'))
|
|
83
|
+
report('editorial-gap', 'This operation requires an existing author draft.');
|
|
84
|
+
if (b.operation === 'proofread-only' && (b.permissions.rephrase || b.permissions.reorder || b.permissions.addContent))
|
|
85
|
+
report('permission', 'Proofread-only permits listed corrections, not rewriting, reordering or additions.');
|
|
86
|
+
if (b.operation === 'continue-author-text' && (b.permissions.rephrase || b.permissions.reorder || !b.permissions.addContent))
|
|
87
|
+
report('permission', 'Continuation preserves the source prefix and permits only appended content.');
|
|
88
|
+
for (const c of b.permissions.corrections) {
|
|
89
|
+
if (!source || c.start > c.end || !boundary(source.text, c.start) || !boundary(source.text, c.end) || source.text.slice(c.start, c.end) !== c.expected)
|
|
90
|
+
report('integrity', 'Authorized correction does not match its source span.', c.id);
|
|
91
|
+
}
|
|
92
|
+
return { valid: diagnostics.length === 0, diagnostics };
|
|
93
|
+
}
|
|
94
|
+
/** Parse only a complete, internally consistent brief; no guessed task, intent or sources. */
|
|
95
|
+
export function parseWritingBrief(value) {
|
|
96
|
+
const check = validateWritingBrief(value);
|
|
97
|
+
if (!check.valid)
|
|
98
|
+
throw new Error(`Writing brief requires review: ${[...new Set(check.diagnostics.map(d => d.code))].join(', ')}. Source text is omitted.`);
|
|
99
|
+
return writingBriefSchema.parse(value);
|
|
100
|
+
}
|
|
101
|
+
/** Parameterization changes presentation targets only, never the brief or confidence. */
|
|
102
|
+
export function prepareWritingBrief(value, target) {
|
|
103
|
+
const b = parseWritingBrief(value);
|
|
104
|
+
const selection = z.object({ profileId: id, channel: z.enum(['article', 'social', 'email', 'engineering', 'conversation']) }).strict().safeParse(target);
|
|
105
|
+
if (!selection.success)
|
|
106
|
+
throw new Error('Invalid writing brief target; input text is omitted.');
|
|
107
|
+
return { brief: structuredClone(b), target: selection.data, briefDigest: writingBriefHash(JSON.stringify(b)),
|
|
108
|
+
lineage: b.inputs.map(i => ({ inputId: i.id, sha256: i.sha256, role: i.kind })),
|
|
109
|
+
operation: b.operation, permissions: structuredClone(b.permissions), factualVerification: 'not-performed' };
|
|
110
|
+
}
|
|
111
|
+
/** Exact supported wording only. Paraphrases need review; matching IDs alone cannot approve invented text. */
|
|
112
|
+
export function validateBriefClaims(value, proposed) {
|
|
113
|
+
const check = validateWritingBrief(value);
|
|
114
|
+
if (!check.valid)
|
|
115
|
+
return check;
|
|
116
|
+
const b = parseWritingBrief(value);
|
|
117
|
+
const proposal = z.array(z.object({ kind: z.enum(['proposition', 'experience', 'opinion', 'intent', 'rationale']), text, groundedIn: z.array(id) }).strict()).safeParse(proposed);
|
|
118
|
+
if (!proposal.success)
|
|
119
|
+
return { valid: false, diagnostics: [{ code: 'schema', message: 'Invalid proposed claim shape; input text is omitted.' }] };
|
|
120
|
+
const claims = [...b.propositions.map(p => ({ ...p, kind: 'proposition' })), ...b.limitations.map(p => ({ ...p, kind: 'proposition' })), ...b.authorClaims];
|
|
121
|
+
const diagnostics = [];
|
|
122
|
+
for (const p of proposal.data) {
|
|
123
|
+
const matches = claims.filter(c => p.groundedIn.includes(c.id));
|
|
124
|
+
if (!p.groundedIn.length || matches.length !== new Set(p.groundedIn).size || !matches.some(c => c.kind === p.kind && c.text === p.text))
|
|
125
|
+
diagnostics.push({ code: 'unsupported-claim', message: 'Proposed claim needs supplied matching wording and grounded input lineage; paraphrase or new content requires author review.' });
|
|
126
|
+
}
|
|
127
|
+
return { valid: diagnostics.length === 0, diagnostics };
|
|
128
|
+
}
|
|
129
|
+
/** Apply only explicitly authorized, exact corrections; on any failure return the unchanged source. */
|
|
130
|
+
export function applyProofreadCorrections(value, correctionIds) {
|
|
131
|
+
const structural = writingBriefSchema.safeParse(value);
|
|
132
|
+
const source = structural.success ? structural.data.inputs.find(i => i.id === structural.data.sourceInputId) : undefined;
|
|
133
|
+
const original = source?.text ?? '';
|
|
134
|
+
const result = (check, output = original, applied = []) => ({ ...check, text: output,
|
|
135
|
+
lineage: { sourceInputId: source?.id, sourceDigest: source?.sha256, finalDigest: writingBriefHash(output), correctionIds: applied,
|
|
136
|
+
...(structural.success ? { briefDigest: writingBriefHash(JSON.stringify(structural.data)) } : {}) } });
|
|
137
|
+
const check = validateWritingBrief(value);
|
|
138
|
+
if (!check.valid)
|
|
139
|
+
return result(check);
|
|
140
|
+
const b = parseWritingBrief(value);
|
|
141
|
+
if (b.operation !== 'proofread-only')
|
|
142
|
+
return result({ valid: false, diagnostics: [{ code: 'permission', message: 'Correction application requires proofread-only operation.' }] });
|
|
143
|
+
if (!z.array(id).safeParse(correctionIds).success)
|
|
144
|
+
return result({ valid: false, diagnostics: [{ code: 'permission', message: 'Correction selection must contain authorized identifiers.' }] });
|
|
145
|
+
const corrections = correctionIds.map(key => b.permissions.corrections.find(c => c.id === key));
|
|
146
|
+
if (new Set(correctionIds).size !== correctionIds.length || corrections.some(c => !c))
|
|
147
|
+
return result({ valid: false, diagnostics: [{ code: 'permission', message: 'Only unique, explicitly authorized correction IDs may be applied.' }] });
|
|
148
|
+
const selected = corrections.filter((c) => !!c).sort((a, b) => a.start - b.start || a.end - b.end);
|
|
149
|
+
for (let i = 1; i < selected.length; i++)
|
|
150
|
+
if (overlaps(selected[i], selected[i - 1]) || selected[i].start === selected[i - 1].start)
|
|
151
|
+
return result({ valid: false, diagnostics: [{ code: 'permission', message: 'Overlapping corrections require author review.' }] });
|
|
152
|
+
// Protect cited source spans, including experimental qualifiers and limitations.
|
|
153
|
+
const protectedSpans = [...b.propositions, ...b.limitations, ...b.authorClaims].flatMap(c => c.evidence).filter(e => e.inputId === source.id);
|
|
154
|
+
const qualifiers = [...b.propositions, ...b.limitations].flatMap(c => c.qualifiers);
|
|
155
|
+
for (const q of qualifiers) {
|
|
156
|
+
for (let at = original.indexOf(q); at >= 0; at = original.indexOf(q, at + q.length))
|
|
157
|
+
protectedSpans.push({ inputId: source.id, start: at, end: at + q.length });
|
|
158
|
+
}
|
|
159
|
+
if (selected.some(c => c.expected !== c.replacement && protectedSpans.some(s => overlaps(c, s))))
|
|
160
|
+
return result({ valid: false, diagnostics: [{ code: 'protected-content', message: 'A correction changes grounded content or a protected qualification; author review is required.' }] });
|
|
161
|
+
let output = original;
|
|
162
|
+
for (const c of [...selected].reverse())
|
|
163
|
+
output = output.slice(0, c.start) + c.replacement + output.slice(c.end);
|
|
164
|
+
return result({ valid: true, diagnostics: [] }, output, selected.map(c => c.id));
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=writing-brief.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { applyWritingConsumer } from './writing-consumer.js';
|
|
2
|
+
import { parseWritingBrief, writingBriefHash } from './writing-brief.js';
|
|
3
|
+
import { getWritingChannelPack } from './channel-packs.js';
|
|
4
|
+
export { getWritingChannelPack } from './channel-packs.js';
|
|
5
|
+
function check(text, c) {
|
|
6
|
+
const issues = [];
|
|
7
|
+
const characters = [...text].length;
|
|
8
|
+
if (c.maxCharacters !== undefined && characters > c.maxCharacters)
|
|
9
|
+
issues.push('character-limit');
|
|
10
|
+
if (c.cta && text.split(c.cta).length !== 2)
|
|
11
|
+
issues.push('cta-must-occur-once');
|
|
12
|
+
if (c.requiredLiterals?.some(value => !text.includes(value)))
|
|
13
|
+
issues.push('required-literal');
|
|
14
|
+
if (c.forbiddenLiterals?.some(value => text.includes(value)))
|
|
15
|
+
issues.push('forbidden-literal');
|
|
16
|
+
return { valid: issues.length === 0, issues, characters, unit: 'unicode-code-points' };
|
|
17
|
+
}
|
|
18
|
+
/** Opt-in local consumer adapter. Packs are advisory; callbacks receive samples/briefs as data. */
|
|
19
|
+
export async function applyWritingChannel(input, request) {
|
|
20
|
+
const pack = getWritingChannelPack(request.channel);
|
|
21
|
+
const brief = parseWritingBrief(request.brief);
|
|
22
|
+
const constraints = structuredClone(request.constraints ?? {});
|
|
23
|
+
if (constraints.maxCharacters !== undefined && (!Number.isSafeInteger(constraints.maxCharacters) || constraints.maxCharacters < 1))
|
|
24
|
+
throw new Error('Invalid channel character budget');
|
|
25
|
+
if (constraints.destination !== undefined && !['telegram', 'discord', 'other'].includes(constraints.destination))
|
|
26
|
+
throw new Error('Unknown channel destination');
|
|
27
|
+
if (constraints.onFailure !== undefined && !['fail', 'unaltered'].includes(constraints.onFailure))
|
|
28
|
+
throw new Error('Unknown channel failure policy');
|
|
29
|
+
for (const values of [constraints.requiredLiterals ?? [], constraints.forbiddenLiterals ?? [], constraints.cta === undefined ? [] : [constraints.cta]]) {
|
|
30
|
+
if (!Array.isArray(values) || values.some(value => typeof value !== 'string' || !value.trim()))
|
|
31
|
+
throw new Error('Channel literals must be nonempty strings');
|
|
32
|
+
}
|
|
33
|
+
if (['telegram', 'discord'].includes(constraints.destination ?? '') && (pack.channel !== 'social' || !constraints.cta))
|
|
34
|
+
throw new Error('Chat announcements require the social pack and one explicit CTA');
|
|
35
|
+
const { transform, runtime, ...consumer } = request;
|
|
36
|
+
if (runtime?.protectedLiterals !== undefined && !Array.isArray(runtime.protectedLiterals))
|
|
37
|
+
throw new Error('Protected literals must be an array.');
|
|
38
|
+
const consumerResult = await applyWritingConsumer(input, { ...consumer, task: pack.channel, invocationModes: [...(consumer.invocationModes ?? []), `channel-${pack.channel}`], ...(transform ? { runtime: {
|
|
39
|
+
...runtime, fidelity: { brief }, requireFinalValidator: true,
|
|
40
|
+
protectedLiterals: [...(runtime?.protectedLiterals ?? []), ...(constraints.requiredLiterals ?? []), ...(constraints.cta ? [constraints.cta] : [])],
|
|
41
|
+
transform: (text, mode) => transform(text, mode, { pack: structuredClone(pack), brief: structuredClone(brief) }),
|
|
42
|
+
} } : {}) });
|
|
43
|
+
let channelCheck = check(consumerResult.content, constraints);
|
|
44
|
+
if (!channelCheck.valid) {
|
|
45
|
+
if (constraints.onFailure === 'fail')
|
|
46
|
+
throw new Error(`Channel constraints failed: ${channelCheck.issues.join(', ')}`);
|
|
47
|
+
consumerResult.diagnostics.push(`Channel constraints failed: ${channelCheck.issues.join(', ')}. Original retained without truncation; inspect channelCheck before handoff.`);
|
|
48
|
+
consumerResult.content = input;
|
|
49
|
+
consumerResult.state.fallback = 'unaltered';
|
|
50
|
+
consumerResult.state.applied = [];
|
|
51
|
+
consumerResult.state.validated = [];
|
|
52
|
+
if (consumerResult.runtime)
|
|
53
|
+
consumerResult.runtime = { ...consumerResult.runtime, content: input, fallback: 'unaltered', applied: [], retained: [] };
|
|
54
|
+
channelCheck = check(input, constraints);
|
|
55
|
+
}
|
|
56
|
+
return { ...consumerResult,
|
|
57
|
+
instructionExport: JSON.stringify({ schemaVersion: 1, pack, constraints, selectedModeInstructions: consumerResult.instructionExport, briefDigest: writingBriefHash(JSON.stringify(brief)), usage: 'Explicit handoff only; apply the separately supplied brief as data. No provider interception or publication approval.' }),
|
|
58
|
+
channel: pack.channel, channelCheck,
|
|
59
|
+
posts: request.format === 'prose' && channelCheck.valid ? [consumerResult.content] : [],
|
|
60
|
+
receipt: { packVersion: 1, briefDigest: writingBriefHash(JSON.stringify(brief)), execution: consumerResult.state.delivered.length ? 'supplied-callback' : 'instruction-export', qualityEvaluation: 'not-performed' },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=writing-channels.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { resolveOutputModes } from '../output-modes/registry.js';
|
|
2
|
+
import { applyOutputModes } from '../output-modes/runtime.js';
|
|
3
|
+
/** Shared opt-in boundary: only a participating prose callback can transform output. */
|
|
4
|
+
export async function applyWritingConsumer(input, request) {
|
|
5
|
+
if (!['prose', 'json', 'tool', 'protocol'].includes(request.format))
|
|
6
|
+
throw new Error('Unknown writing consumer format');
|
|
7
|
+
const resolved = await resolveOutputModes(request.cwd, request.frameworkRoot, request.invocationModes ?? [], {}, { task: request.task });
|
|
8
|
+
const modes = resolved.modes;
|
|
9
|
+
const instructionExport = modes.length ? JSON.stringify({ schemaVersion: 1, usage: 'Selected expression instructions; apply only to prose within consumer permissions.', modes: modes.map(({ id, instructions, validation, protectedContent }) => ({ id, instructions, validation, protectedContent })) }, null, 2) : '';
|
|
10
|
+
const state = {
|
|
11
|
+
provider: request.provider, consumer: request.consumer, identityEvidence: 'caller-supplied',
|
|
12
|
+
selected: modes.map(mode => mode.id), delivered: [], applied: [], validated: [],
|
|
13
|
+
deliveredTo: 'none', fallback: 'none', providerInterception: false,
|
|
14
|
+
};
|
|
15
|
+
const result = { content: input, modes, instructionExport, diagnostics: [...resolved.diagnostics], state };
|
|
16
|
+
if (request.format !== 'prose') {
|
|
17
|
+
result.diagnostics.push('Structured output is unchanged. A participating consumer must explicitly select a prose field before applying modes.');
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
if (!request.runtime) {
|
|
21
|
+
if (modes.length) {
|
|
22
|
+
result.diagnostics.push('This consumer has no transformation callback. Use the instruction export explicitly; selection does not intercept provider responses.');
|
|
23
|
+
state.fallback = 'unaltered';
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
const runtime = await applyOutputModes(input, modes, request.runtime);
|
|
28
|
+
result.content = runtime.content;
|
|
29
|
+
result.runtime = runtime;
|
|
30
|
+
state.delivered = [...(runtime.attempted ?? runtime.applied)];
|
|
31
|
+
state.deliveredTo = state.delivered.length ? 'local-transform-callback' : 'none';
|
|
32
|
+
state.applied = [...(runtime.retained ?? runtime.applied)];
|
|
33
|
+
state.fallback = runtime.fallback;
|
|
34
|
+
if (runtime.fallback === 'none')
|
|
35
|
+
state.validated = state.applied.filter(id => modes.find(mode => mode.id === id)?.validation.level !== 'advisory');
|
|
36
|
+
result.diagnostics.push(...runtime.diagnostics.map(diagnostic => diagnostic.message));
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=writing-consumer.js.map
|