@supersuit/hyperspec 0.6.0 → 0.7.0
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/CHANGELOG.md +91 -0
- package/README.md +37 -0
- package/SPEC.md +2 -2
- package/WRITING.md +542 -10
- package/bin/hyperspec.mjs +183 -0
- package/examples/writing/essay/judge/doctor.packet.json +108 -0
- package/examples/writing/essay/judge/lineup.packet.json +64 -0
- package/examples/writing/essay/judge/persona.packet.json +73 -0
- package/examples/writing/essay/judge/reader.packet.json +93 -0
- package/examples/writing/essay/learn/first-draft.md +84 -0
- package/examples/writing/essay/learn/learn.packet.json +106 -0
- package/examples/writing/essay/sample-verdicts/doctor.verdict.json +43 -0
- package/examples/writing/essay/sample-verdicts/learn.verdict.json +30 -0
- package/examples/writing/essay/sample-verdicts/lineup.verdict.json +6 -0
- package/examples/writing/essay/sample-verdicts/persona.verdict.json +4 -0
- package/examples/writing/essay/sample-verdicts/reader.verdict.json +7 -0
- package/examples/writing/essay.hyperspec.md +6 -1
- package/examples/writing/story/judge/attribution.packet.json +194 -0
- package/examples/writing/story/judge/doctor.packet.json +108 -0
- package/examples/writing/story/judge/knowledge.packet.json +77 -0
- package/examples/writing/story/judge/persona.packet.json +73 -0
- package/examples/writing/story/judge/reader.packet.json +94 -0
- package/examples/writing/story/sample-verdicts/attribution.verdict.json +81 -0
- package/examples/writing/story/sample-verdicts/doctor.verdict.json +43 -0
- package/examples/writing/story/sample-verdicts/knowledge.verdict.json +4 -0
- package/examples/writing/story/sample-verdicts/persona.verdict.json +20 -0
- package/examples/writing/story/sample-verdicts/reader.verdict.json +16 -0
- package/examples/writing/story.hyperspec.md +7 -3
- package/package.json +1 -1
- package/src/check.mjs +75 -129
- package/src/draft.mjs +26 -0
- package/src/judge.mjs +386 -0
- package/src/judges/attribution.mjs +360 -0
- package/src/judges/doctor.mjs +126 -0
- package/src/judges/index.mjs +31 -0
- package/src/judges/knowledge.mjs +111 -0
- package/src/judges/lineup.mjs +272 -0
- package/src/judges/persona.mjs +137 -0
- package/src/judges/reader.mjs +111 -0
- package/src/learn.mjs +422 -0
- package/src/ledger.mjs +108 -0
- package/src/sentences.mjs +81 -0
- package/src/stations/claims.mjs +44 -39
- package/src/stations/quotes.mjs +6 -4
- package/src/writing.mjs +1 -1
package/src/stations/claims.mjs
CHANGED
|
@@ -44,6 +44,44 @@ function firstOccurrence(text, claimText) {
|
|
|
44
44
|
return m ? m.index : -1;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// The claims ledger as the claims station reads it, shared with the persona judge
|
|
48
|
+
// (src/judges/persona.mjs), so both see the same claims. { path, missing, lines }: path is
|
|
49
|
+
// writing.sources.ledger as written (null when unset); missing is true when it is set but cannot be
|
|
50
|
+
// read; lines holds every non-blank line, 1-based as n, each either { n, text } (a claim with
|
|
51
|
+
// non-empty text; claim is the parsed object) or { n, problem } naming why it is not one.
|
|
52
|
+
export function readClaimsLedger(spec) {
|
|
53
|
+
const ledgerPath = str(spec?.data?.writing?.sources?.ledger);
|
|
54
|
+
if (!ledgerPath) return { path: null, missing: false, lines: [] };
|
|
55
|
+
let raw;
|
|
56
|
+
try {
|
|
57
|
+
// A leading UTF-8 BOM (written by default by several Windows/Excel-adjacent editors) is not
|
|
58
|
+
// valid JSON leading whitespace, so it must come off before line 1 is parsed, or a genuinely
|
|
59
|
+
// well-formed first line reports as broken JSON for a reason that has nothing to do with its
|
|
60
|
+
// content.
|
|
61
|
+
raw = readFileSync(resolve(spec?.dir || ".", ledgerPath), "utf8").replace(/^\uFEFF/, "");
|
|
62
|
+
} catch {
|
|
63
|
+
return { path: ledgerPath, missing: true, lines: [] };
|
|
64
|
+
}
|
|
65
|
+
const lines = [];
|
|
66
|
+
raw.split("\n").forEach((text, i) => {
|
|
67
|
+
if (text.trim() === "") return;
|
|
68
|
+
const n = i + 1;
|
|
69
|
+
let obj;
|
|
70
|
+
try { obj = JSON.parse(text); } catch { lines.push({ n, problem: "is not valid JSON" }); return; }
|
|
71
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) { lines.push({ n, problem: "is not a JSON object" }); return; }
|
|
72
|
+
const claimText = typeof obj.text === "string" ? obj.text : "";
|
|
73
|
+
if (!claimText.trim()) { lines.push({ n, problem: "has no text" }); return; }
|
|
74
|
+
lines.push({ n, text: claimText, claim: obj });
|
|
75
|
+
});
|
|
76
|
+
return { path: ledgerPath, missing: false, lines };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const PROBLEM_FIX = {
|
|
80
|
+
"is not valid JSON": "Fix the JSON on that line.",
|
|
81
|
+
"is not a JSON object": 'Each ledger line must be a JSON object: {"text": "...", "source": "..."}.',
|
|
82
|
+
"has no text": "Add text: the claim exactly as it appears in the draft.",
|
|
83
|
+
};
|
|
84
|
+
|
|
47
85
|
export function run(spec, draft) {
|
|
48
86
|
const sources = spec?.data?.writing?.sources ?? {};
|
|
49
87
|
const ledgerPath = str(sources.ledger);
|
|
@@ -57,15 +95,8 @@ export function run(spec, draft) {
|
|
|
57
95
|
return { station: name, status: "skip", findings: [], reason: "writing.sources.ledger is not set" };
|
|
58
96
|
}
|
|
59
97
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
// A leading UTF-8 BOM (written by default by several Windows/Excel-adjacent editors) is not
|
|
64
|
-
// valid JSON leading whitespace, so it must come off before line 1 is parsed, or a genuinely
|
|
65
|
-
// well-formed first line reports as broken JSON for a reason that has nothing to do with its
|
|
66
|
-
// content.
|
|
67
|
-
raw = readFileSync(ledgerAbs, "utf8").replace(/^/, "");
|
|
68
|
-
} catch {
|
|
98
|
+
const ledger = readClaimsLedger(spec);
|
|
99
|
+
if (ledger.missing) {
|
|
69
100
|
return {
|
|
70
101
|
station: name,
|
|
71
102
|
status: "fail",
|
|
@@ -81,41 +112,15 @@ export function run(spec, draft) {
|
|
|
81
112
|
|
|
82
113
|
const findings = [];
|
|
83
114
|
const draftNorm = normalize(draft.text);
|
|
84
|
-
const ledgerLines = raw.split("\n").map((text, i) => ({ n: i + 1, text })).filter((l) => l.text.trim() !== "");
|
|
85
|
-
|
|
86
|
-
for (const { n, text } of ledgerLines) {
|
|
87
|
-
let obj;
|
|
88
|
-
try {
|
|
89
|
-
obj = JSON.parse(text);
|
|
90
|
-
} catch {
|
|
91
|
-
findings.push({
|
|
92
|
-
station: name,
|
|
93
|
-
id: `station-claims-json-line-${n}`,
|
|
94
|
-
severity: "fail",
|
|
95
|
-
message: `writing.sources.ledger "${ledgerPath}" line ${n} is not valid JSON`,
|
|
96
|
-
fix: "Fix the JSON on that line.",
|
|
97
|
-
});
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
101
|
-
findings.push({
|
|
102
|
-
station: name,
|
|
103
|
-
id: `station-claims-json-line-${n}`,
|
|
104
|
-
severity: "fail",
|
|
105
|
-
message: `writing.sources.ledger "${ledgerPath}" line ${n} is not a JSON object`,
|
|
106
|
-
fix: 'Each ledger line must be a JSON object: {"text": "...", "source": "..."}.',
|
|
107
|
-
});
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
if (
|
|
116
|
+
for (const { n, problem, text: claimText, claim: obj } of ledger.lines) {
|
|
117
|
+
if (problem) {
|
|
113
118
|
findings.push({
|
|
114
119
|
station: name,
|
|
115
120
|
id: `station-claims-json-line-${n}`,
|
|
116
121
|
severity: "fail",
|
|
117
|
-
message: `writing.sources.ledger "${ledgerPath}" line ${n}
|
|
118
|
-
fix:
|
|
122
|
+
message: `writing.sources.ledger "${ledgerPath}" line ${n} ${problem}`,
|
|
123
|
+
fix: PROBLEM_FIX[problem],
|
|
119
124
|
});
|
|
120
125
|
continue;
|
|
121
126
|
}
|
package/src/stations/quotes.mjs
CHANGED
|
@@ -59,15 +59,17 @@ function normalize(text) {
|
|
|
59
59
|
// what was said.
|
|
60
60
|
const matchKey = (inner) => normalize(inner).replace(/[.,]+$/, "").trim();
|
|
61
61
|
|
|
62
|
-
// Every quoted span in `text`, as { start, end, inner }: start/end bound the whole span
|
|
63
|
-
// its quote marks, inner is the text between them
|
|
64
|
-
|
|
62
|
+
// Every quoted span in `text`, as { start, end, inner, para }: start/end bound the whole span
|
|
63
|
+
// including its quote marks, inner is the text between them, para is { start, end } of the
|
|
64
|
+
// paragraph holding it. Paired per paragraph (see the header). Also how the attribution judge
|
|
65
|
+
// (src/judges/attribution.mjs) finds a draft's dialogue lines.
|
|
66
|
+
export function quotedSpans(text) {
|
|
65
67
|
const spans = [];
|
|
66
68
|
for (const para of splitSegments(text, { by: "paragraph" })) {
|
|
67
69
|
QUOTE_RE.lastIndex = 0;
|
|
68
70
|
let m;
|
|
69
71
|
while ((m = QUOTE_RE.exec(para.text))) {
|
|
70
|
-
spans.push({ start: para.start + m.index, end: para.start + m.index + m[0].length, inner: m[1] ?? m[2] ?? "" });
|
|
72
|
+
spans.push({ start: para.start + m.index, end: para.start + m.index + m[0].length, inner: m[1] ?? m[2] ?? "", para: { start: para.start, end: para.end } });
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
return spans;
|
package/src/writing.mjs
CHANGED
|
@@ -36,7 +36,7 @@ const required = (block, fiction) => block !== "characters" || fiction;
|
|
|
36
36
|
// least one field. This is deliberately shallow: it is the bar for "something was written here",
|
|
37
37
|
// not the bar for "this block is correct", which is what checkOwner and the field rules in
|
|
38
38
|
// writing-fields.mjs are for.
|
|
39
|
-
function blockPresent(block, raw) {
|
|
39
|
+
export function blockPresent(block, raw) {
|
|
40
40
|
if (block === "characters") return Array.isArray(raw) && raw.length > 0;
|
|
41
41
|
if (block === "materials") return isObj(raw) && Array.isArray(raw.items) && raw.items.length > 0;
|
|
42
42
|
return isObj(raw) && Object.keys(raw).length > 0;
|