@inflexa-ai/harness 0.17.0 → 0.19.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/dist/agents/report-builder.d.ts +2 -0
- package/dist/agents/report-builder.js +2 -0
- package/dist/agents/report-session-agent.d.ts +56 -0
- package/dist/agents/report-session-agent.js +72 -0
- package/dist/app/report-session-runtime.d.ts +72 -0
- package/dist/app/report-session-runtime.js +157 -0
- package/dist/app/spawn-report-session.d.ts +85 -0
- package/dist/app/spawn-report-session.js +83 -0
- package/dist/contracts/chat-events.d.ts +9 -0
- package/dist/contracts/index.d.ts +6 -2
- package/dist/contracts/index.js +3 -1
- package/dist/contracts/organ-system.d.ts +49 -0
- package/dist/contracts/organ-system.js +67 -0
- package/dist/contracts/report-blocks.d.ts +564 -0
- package/dist/contracts/report-blocks.js +117 -0
- package/dist/contracts/report-reference.d.ts +459 -0
- package/dist/contracts/report-reference.js +256 -0
- package/dist/contracts/schemas/chat-events.d.ts +2 -0
- package/dist/contracts/schemas/chat-events.js +2 -0
- package/dist/contracts/severity.d.ts +15 -0
- package/dist/contracts/severity.js +10 -0
- package/dist/contracts/target-dossier.d.ts +8675 -12972
- package/dist/contracts/target-dossier.js +534 -591
- package/dist/data/safety-panel-schema.d.ts +18 -5
- package/dist/data/safety-panel-schema.js +4 -4
- package/dist/execution/artifact-metadata.js +1 -0
- package/dist/execution/report-runner.js +1 -3
- package/dist/execution/run-synthesis.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +19 -0
- package/dist/lib/async-utils.d.ts +16 -4
- package/dist/lib/async-utils.js +20 -15
- package/dist/lib/host-concurrency.js +1 -0
- package/dist/loop/__fixtures__/scripted-provider.js +23 -2
- package/dist/loop/run-agent.js +58 -10
- package/dist/loop/tool-detail.d.ts +40 -0
- package/dist/loop/tool-detail.js +35 -3
- package/dist/loop/types.d.ts +13 -0
- package/dist/memory/thread-history.d.ts +10 -0
- package/dist/memory/thread-history.js +12 -1
- package/dist/memory/thread-store.d.ts +4 -4
- package/dist/memory/thread-store.js +18 -9
- package/dist/prompts/report-session.d.ts +1 -0
- package/dist/prompts/report-session.js +89 -0
- package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
- package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
- package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
- package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
- package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
- package/dist/prompts/target-assessment/investigation/index.js +3 -0
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
- package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
- package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
- package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
- package/dist/providers/__fixtures__/session.d.ts +5 -4
- package/dist/providers/__fixtures__/session.js +4 -4
- package/dist/providers/ai-sdk.d.ts +77 -0
- package/dist/providers/ai-sdk.js +149 -19
- package/dist/providers/errors.d.ts +14 -0
- package/dist/providers/errors.js +103 -18
- package/dist/providers/types.d.ts +16 -0
- package/dist/providers/types.js +10 -1
- package/dist/report-model/block-walk.d.ts +56 -0
- package/dist/report-model/block-walk.js +89 -0
- package/dist/report-model/draft-finish.d.ts +60 -0
- package/dist/report-model/draft-finish.js +56 -0
- package/dist/report-model/draft-operations.d.ts +125 -0
- package/dist/report-model/draft-operations.js +341 -0
- package/dist/report-model/draft-read.d.ts +59 -0
- package/dist/report-model/draft-read.js +109 -0
- package/dist/report-model/draft.d.ts +51 -0
- package/dist/report-model/draft.js +32 -0
- package/dist/report-model/fixture-resolver.d.ts +8 -0
- package/dist/report-model/fixture-resolver.js +252 -0
- package/dist/report-model/pin-snapshot.d.ts +30 -0
- package/dist/report-model/pin-snapshot.js +40 -0
- package/dist/report-model/reference-resolver.d.ts +113 -0
- package/dist/report-model/reference-resolver.js +60 -0
- package/dist/report-model/structural-validation.d.ts +22 -0
- package/dist/report-model/structural-validation.js +68 -0
- package/dist/report-model/validate.d.ts +72 -0
- package/dist/report-model/validate.js +95 -0
- package/dist/report-render/chart.d.ts +33 -0
- package/dist/report-render/chart.js +538 -0
- package/dist/report-render/page.d.ts +127 -0
- package/dist/report-render/page.js +307 -0
- package/dist/report-render/references.d.ts +32 -0
- package/dist/report-render/references.js +44 -0
- package/dist/report-render/render.d.ts +25 -0
- package/dist/report-render/render.js +125 -0
- package/dist/report-render/script-json.d.ts +13 -0
- package/dist/report-render/script-json.js +15 -0
- package/dist/report-render/types.d.ts +39 -0
- package/dist/report-render/types.js +10 -0
- package/dist/report-render/value-bridge.d.ts +62 -0
- package/dist/report-render/value-bridge.js +82 -0
- package/dist/report-render/views/chart-view.d.ts +11 -0
- package/dist/report-render/views/chart-view.js +23 -0
- package/dist/report-render/views/page-view.d.ts +20 -0
- package/dist/report-render/views/page-view.js +49 -0
- package/dist/report-render/views/prose.d.ts +28 -0
- package/dist/report-render/views/prose.js +75 -0
- package/dist/report-render/views/references-view.d.ts +20 -0
- package/dist/report-render/views/references-view.js +64 -0
- package/dist/report-render/views/values.d.ts +42 -0
- package/dist/report-render/views/values.js +43 -0
- package/dist/runtime/assemble.d.ts +15 -0
- package/dist/runtime/assemble.js +28 -6
- package/dist/runtime/boot.d.ts +3 -3
- package/dist/runtime/boot.js +15 -4
- package/dist/runtime/dbos.d.ts +4 -4
- package/dist/state/artifacts.d.ts +16 -0
- package/dist/state/artifacts.js +18 -0
- package/dist/state/init.js +54 -0
- package/dist/state/purge-analysis.js +2 -0
- package/dist/state/report-session-state.d.ts +112 -0
- package/dist/state/report-session-state.js +150 -0
- package/dist/state/report-versions.d.ts +129 -0
- package/dist/state/report-versions.js +167 -0
- package/dist/state/snapshot-parse.d.ts +34 -0
- package/dist/state/snapshot-parse.js +82 -0
- package/dist/tasks/data-profile.js +1 -0
- package/dist/tools/ad-hoc-router.d.ts +7 -2
- package/dist/tools/ad-hoc-router.js +2 -1
- package/dist/tools/bio/chembl.js +1 -0
- package/dist/tools/bio/comptox.js +1 -0
- package/dist/tools/bio/drug-gene-interactions.js +1 -0
- package/dist/tools/bio/gene-disease-evidence.js +1 -0
- package/dist/tools/bio/gene-preclinical-profile.js +1 -0
- package/dist/tools/bio/keys.d.ts +3 -2
- package/dist/tools/bio/keys.js +3 -2
- package/dist/tools/bio/lookup-annotation.js +1 -0
- package/dist/tools/bio/opentargets.d.ts +3 -3
- package/dist/tools/bio/opentargets.js +4 -3
- package/dist/tools/bio/pubchem.js +1 -0
- package/dist/tools/bio/search-clinical-trials.js +1 -0
- package/dist/tools/bio/search-faers.js +1 -0
- package/dist/tools/bio/search-geo-datasets.js +1 -0
- package/dist/tools/bio/search-interactions.js +1 -0
- package/dist/tools/bio/target-safety.d.ts +4 -6
- package/dist/tools/bio/target-safety.js +7 -6
- package/dist/tools/define-tool.d.ts +19 -12
- package/dist/tools/define-tool.js +4 -2
- package/dist/tools/display/show-user.js +1 -0
- package/dist/tools/iterate-report.js +2 -0
- package/dist/tools/lib/chembl-client.d.ts +2 -0
- package/dist/tools/lib/chembl-client.js +1 -0
- package/dist/tools/lib/monarch-client.d.ts +42 -0
- package/dist/tools/lib/monarch-client.js +100 -0
- package/dist/tools/report/build-report.js +1 -0
- package/dist/tools/report/mint-preview-url.js +1 -0
- package/dist/tools/report/preview-snapshot.js +1 -0
- package/dist/tools/report/submit-report.js +1 -0
- package/dist/tools/report/version-fs.js +4 -0
- package/dist/tools/report-authoring/authoring-tools.d.ts +241 -0
- package/dist/tools/report-authoring/authoring-tools.js +460 -0
- package/dist/tools/report-session/index.d.ts +8 -0
- package/dist/tools/report-session/index.js +8 -0
- package/dist/tools/report-session/preview-report.d.ts +90 -0
- package/dist/tools/report-session/preview-report.js +245 -0
- package/dist/tools/research/context7-docs.js +2 -0
- package/dist/tools/research/generate-analogy-report.js +1 -0
- package/dist/tools/research/generate-plan.js +7 -2
- package/dist/tools/research/inspect-data-profile.js +4 -0
- package/dist/tools/research/literature-reviewer.js +1 -0
- package/dist/tools/research/search-arxiv.js +1 -0
- package/dist/tools/research/search-github-repos.js +1 -0
- package/dist/tools/research/search-semantic-scholar.js +1 -0
- package/dist/tools/sandbox/list-available-packages.js +23 -0
- package/dist/tools/sandbox/list-available-refs.js +36 -0
- package/dist/tools/sandbox/report-blocker.js +1 -0
- package/dist/tools/sandbox/skills.js +2 -0
- package/dist/tools/workspace/file-stat.js +1 -0
- package/dist/tools/workspace/show-file.js +2 -0
- package/dist/tools/workspace/show-plan.js +1 -0
- package/dist/workflows/execute-analysis.d.ts +5 -1
- package/dist/workflows/execute-analysis.js +5 -5
- package/dist/workflows/execute-target-assessment.d.ts +13 -2
- package/dist/workflows/execute-target-assessment.js +78 -16
- package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
- package/dist/workflows/target-assessment/assemblers/index.js +1 -1
- package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
- package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
- package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
- package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
- package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
- package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
- package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
- package/dist/workflows/target-assessment/collectors/index.js +48 -5
- package/dist/workflows/target-assessment/coverage.d.ts +43 -27
- package/dist/workflows/target-assessment/coverage.js +33 -29
- package/dist/workflows/target-assessment/fanout/index.js +1 -0
- package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
- package/dist/workflows/target-assessment/investigation/index.js +419 -0
- package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
- package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
- package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
- package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
- package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
- package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
- package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
- package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
- package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
- package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
- package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
- package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
- package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
- package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
- package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
- package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
- package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
- package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
- package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
- package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
- package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
- package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
- package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
- package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
- package/dist/workflows/target-assessment/metrics.d.ts +1 -6
- package/dist/workflows/target-assessment/metrics.js +1 -13
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +2147 -807
- package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
- package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
- package/dist/workflows/target-assessment/phase5-persist.js +56 -173
- package/dist/workflows/target-assessment/schemas.d.ts +46 -1
- package/dist/workflows/target-assessment/schemas.js +19 -0
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +26 -62
- package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +27 -154
- package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
- package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
- package/dist/workflows/target-assessment/synthesis/index.js +4 -1
- package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
- package/package.json +4 -1
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure operations that compose a draft report document.
|
|
3
|
+
*
|
|
4
|
+
* An operation takes the draft, an operation value, and the pinned snapshot. It gives a new draft, or a
|
|
5
|
+
* typed refusal in the error channel. It mutates nothing. Thus a refused operation leaves the input draft
|
|
6
|
+
* as it was.
|
|
7
|
+
*
|
|
8
|
+
* An operation validates the delta only. It parses the new payload with the draft grammar. It scans the
|
|
9
|
+
* ids that the payload brings, and it resolves each new reference against the snapshot with the
|
|
10
|
+
* structural tier. The completeness rules gate one time, at the finish.
|
|
11
|
+
*
|
|
12
|
+
* The candidate shares each untouched branch with the input draft. An operation rebuilds only the path to
|
|
13
|
+
* the edit. Thus the input stays intact, and the memory cost stays small.
|
|
14
|
+
*
|
|
15
|
+
* A destination names an anchor, not an index. An index breaks when the tree changes under the agent, and
|
|
16
|
+
* an anchor id stays stable.
|
|
17
|
+
*/
|
|
18
|
+
import { err, ok } from "neverthrow";
|
|
19
|
+
import { walkBlocks } from "./block-walk.js";
|
|
20
|
+
import { DraftBlockSchema } from "./draft.js";
|
|
21
|
+
import { validateReferenceStructure } from "./structural-validation.js";
|
|
22
|
+
/**
|
|
23
|
+
* Set the title of the document.
|
|
24
|
+
*
|
|
25
|
+
* A title carries no id and no reference, thus it needs none of the shared checks and it cannot refuse.
|
|
26
|
+
* The document schema requires a non-empty title, and the finish reports an empty one as a gap.
|
|
27
|
+
*/
|
|
28
|
+
export function setTitle(draft, title) {
|
|
29
|
+
return { ...draft, title };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Add a block at a destination.
|
|
33
|
+
*
|
|
34
|
+
* The root admits a section only, thus an atom at the root refuses. The validation covers the payload and
|
|
35
|
+
* each of its descendants.
|
|
36
|
+
*/
|
|
37
|
+
export function addBlock(draft, operation, snapshot) {
|
|
38
|
+
const parsed = DraftBlockSchema.safeParse(operation.block);
|
|
39
|
+
if (!parsed.success) {
|
|
40
|
+
return err(malformed(parsed.error));
|
|
41
|
+
}
|
|
42
|
+
const block = parsed.data;
|
|
43
|
+
return resolveDestination(draft, operation.destination).andThen((destination) => {
|
|
44
|
+
if (destination.parentIsRoot && block.kind !== "section") {
|
|
45
|
+
return err({ reason: "atom-at-root", detail: `a ${block.kind} block cannot sit at the root` });
|
|
46
|
+
}
|
|
47
|
+
const candidate = insertAt(draft, destination.containerPath, destination.index, block);
|
|
48
|
+
return commit(candidate, block, snapshot);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Change one block by its id.
|
|
53
|
+
*
|
|
54
|
+
* A section target takes a title, and its children stay as they are. An atom target takes a full block,
|
|
55
|
+
* and a kind change is permitted. A payload that does not match the target kind refuses.
|
|
56
|
+
*/
|
|
57
|
+
export function changeBlock(draft, operation, snapshot) {
|
|
58
|
+
const target = locate(draft, operation.targetId);
|
|
59
|
+
if (target === undefined) {
|
|
60
|
+
return err({ reason: "unknown-target", detail: `no block holds the id ${operation.targetId}` });
|
|
61
|
+
}
|
|
62
|
+
const containerPath = target.path.slice(0, -1);
|
|
63
|
+
const index = target.path[target.path.length - 1];
|
|
64
|
+
if (target.block.kind === "section") {
|
|
65
|
+
if (!("title" in operation)) {
|
|
66
|
+
return err({ reason: "payload-kind-mismatch", detail: `the section ${operation.targetId} takes a title, not a block payload` });
|
|
67
|
+
}
|
|
68
|
+
const retitled = { ...target.block, title: operation.title };
|
|
69
|
+
return commit(replaceAt(draft, containerPath, index, retitled), undefined, snapshot);
|
|
70
|
+
}
|
|
71
|
+
if ("title" in operation) {
|
|
72
|
+
return err({ reason: "payload-kind-mismatch", detail: `the block ${operation.targetId} is an atom, and it takes a full block, not a title` });
|
|
73
|
+
}
|
|
74
|
+
const payload = operation.block;
|
|
75
|
+
if (payload === null || typeof payload !== "object") {
|
|
76
|
+
return err({ reason: "malformed-block", detail: "the payload is not an object" });
|
|
77
|
+
}
|
|
78
|
+
// The stamp sets the target id on the payload before the parse. Thus an id mismatch is
|
|
79
|
+
// unrepresentable, and a replacement always keeps the id of its target.
|
|
80
|
+
const stamped = { ...payload, id: operation.targetId };
|
|
81
|
+
const parsed = DraftBlockSchema.safeParse(stamped);
|
|
82
|
+
if (!parsed.success) {
|
|
83
|
+
return err(malformed(parsed.error));
|
|
84
|
+
}
|
|
85
|
+
if (parsed.data.kind === "section") {
|
|
86
|
+
return err({ reason: "payload-kind-mismatch", detail: `the block ${operation.targetId} is an atom, and it cannot take a section payload` });
|
|
87
|
+
}
|
|
88
|
+
return commit(replaceAt(draft, containerPath, index, parsed.data), parsed.data, snapshot);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Remove one block by its id.
|
|
92
|
+
*
|
|
93
|
+
* A removed section takes its whole subtree. A removed id is free for a later add.
|
|
94
|
+
*/
|
|
95
|
+
export function removeBlock(draft, operation, snapshot) {
|
|
96
|
+
const target = locate(draft, operation.targetId);
|
|
97
|
+
if (target === undefined) {
|
|
98
|
+
return err({ reason: "unknown-target", detail: `no block holds the id ${operation.targetId}` });
|
|
99
|
+
}
|
|
100
|
+
const containerPath = target.path.slice(0, -1);
|
|
101
|
+
const index = target.path[target.path.length - 1];
|
|
102
|
+
return commit(removeAt(draft, containerPath, index), undefined, snapshot);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Move one block to a destination.
|
|
106
|
+
*
|
|
107
|
+
* The moved block keeps its subtree. A section must not move into its own subtree, thus a move into the
|
|
108
|
+
* subtree refuses with a cycle. A move with the moved block as its own anchor refuses.
|
|
109
|
+
*/
|
|
110
|
+
export function moveBlock(draft, operation, snapshot) {
|
|
111
|
+
const target = locate(draft, operation.targetId);
|
|
112
|
+
if (target === undefined) {
|
|
113
|
+
return err({ reason: "unknown-target", detail: `no block holds the id ${operation.targetId}` });
|
|
114
|
+
}
|
|
115
|
+
const movedBlock = target.block;
|
|
116
|
+
const movedPath = target.path;
|
|
117
|
+
const place = operation.destination?.place;
|
|
118
|
+
if (place !== undefined && typeof place === "object") {
|
|
119
|
+
const anchorId = "before" in place ? place.before : place.after;
|
|
120
|
+
if (anchorId === operation.targetId) {
|
|
121
|
+
return err({ reason: "unknown-target", detail: `the block ${operation.targetId} cannot anchor its own move` });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return resolveDestination(draft, operation.destination).andThen((destination) => {
|
|
125
|
+
if (destination.parentIsRoot && movedBlock.kind !== "section") {
|
|
126
|
+
return err({ reason: "atom-at-root", detail: `a ${movedBlock.kind} block cannot sit at the root` });
|
|
127
|
+
}
|
|
128
|
+
// The moved block spans each path that starts with its own path. A destination inside that span is
|
|
129
|
+
// a move into the block's own subtree, thus it is a cycle.
|
|
130
|
+
if (movedBlock.kind === "section" && isPrefix(movedPath, destination.containerPath)) {
|
|
131
|
+
return err({ reason: "cycle", detail: `the section ${operation.targetId} cannot move into its own subtree` });
|
|
132
|
+
}
|
|
133
|
+
const afterRemove = removeAt(draft, movedPath.slice(0, -1), movedPath[movedPath.length - 1]);
|
|
134
|
+
// The removal shifts the sibling indices of the old parent. Thus the destination resolves again
|
|
135
|
+
// against the reduced tree, and the fresh path and index address the correct place.
|
|
136
|
+
return resolveDestination(afterRemove, operation.destination).andThen((landing) => {
|
|
137
|
+
const candidate = insertAt(afterRemove, landing.containerPath, landing.index, movedBlock);
|
|
138
|
+
return commit(candidate, undefined, snapshot);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Resolve a destination to a container path and an insert index.
|
|
144
|
+
*
|
|
145
|
+
* An anchor place implies its parent. A named parent that does not hold the anchor refuses. A named
|
|
146
|
+
* parent that is not a section refuses.
|
|
147
|
+
*/
|
|
148
|
+
function resolveDestination(document, destination) {
|
|
149
|
+
const place = destination?.place ?? "end";
|
|
150
|
+
const parentId = destination?.parentId;
|
|
151
|
+
if (typeof place === "object") {
|
|
152
|
+
const anchorId = "before" in place ? place.before : place.after;
|
|
153
|
+
const anchor = locate(document, anchorId);
|
|
154
|
+
if (anchor === undefined) {
|
|
155
|
+
return err({ reason: "unknown-target", detail: `no block holds the anchor id ${anchorId}` });
|
|
156
|
+
}
|
|
157
|
+
const containerPath = anchor.path.slice(0, -1);
|
|
158
|
+
const anchorIndex = anchor.path[anchor.path.length - 1];
|
|
159
|
+
const parentIsRoot = containerPath.length === 0;
|
|
160
|
+
if (parentId !== undefined && idAtPath(document, containerPath) !== parentId) {
|
|
161
|
+
return err({ reason: "unknown-target", detail: `the parent ${parentId} does not hold the anchor ${anchorId}` });
|
|
162
|
+
}
|
|
163
|
+
const index = "before" in place ? anchorIndex : anchorIndex + 1;
|
|
164
|
+
return ok({ containerPath, index, parentIsRoot });
|
|
165
|
+
}
|
|
166
|
+
if (parentId === undefined) {
|
|
167
|
+
const index = place === "start" ? 0 : document.sections.length;
|
|
168
|
+
return ok({ containerPath: [], index, parentIsRoot: true });
|
|
169
|
+
}
|
|
170
|
+
const parent = locate(document, parentId);
|
|
171
|
+
if (parent === undefined) {
|
|
172
|
+
return err({ reason: "unknown-target", detail: `no block holds the parent id ${parentId}` });
|
|
173
|
+
}
|
|
174
|
+
if (parent.block.kind !== "section") {
|
|
175
|
+
return err({ reason: "not-a-section", detail: `the block ${parentId} is a ${parent.block.kind}, not a section` });
|
|
176
|
+
}
|
|
177
|
+
const index = place === "start" ? 0 : parent.block.blocks.length;
|
|
178
|
+
return ok({ containerPath: parent.path, index, parentIsRoot: false });
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Run the shared checks over a candidate, and give the candidate or a refusal.
|
|
182
|
+
*
|
|
183
|
+
* `added` is the block that the operation puts into the candidate, or `undefined` when the operation
|
|
184
|
+
* brings no block. Both checks read the delta, because each block that was already in the draft passed
|
|
185
|
+
* them on its own landing.
|
|
186
|
+
*
|
|
187
|
+
* The id scan counts the ids of `added` inside the candidate, and it refuses when one of them occurs more
|
|
188
|
+
* than one time. A whole-draft scan would be wrong here: one duplicate that the draft already carries
|
|
189
|
+
* would then refuse every later operation, including an operation on an untouched branch, and the agent
|
|
190
|
+
* would read a refusal that names an id it never touched.
|
|
191
|
+
*/
|
|
192
|
+
function commit(candidate, added, snapshot) {
|
|
193
|
+
if (added === undefined) {
|
|
194
|
+
return ok(candidate);
|
|
195
|
+
}
|
|
196
|
+
const addedIds = [];
|
|
197
|
+
collectIds([added], addedIds);
|
|
198
|
+
const counts = idCounts(candidate);
|
|
199
|
+
for (const id of addedIds) {
|
|
200
|
+
if ((counts.get(id) ?? 0) > 1) {
|
|
201
|
+
return err({ reason: "duplicate-id", detail: `the id ${id} occurs more than one time in the draft` });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const unresolved = [];
|
|
205
|
+
for (const entry of walkBlocks([added]).references) {
|
|
206
|
+
validateReferenceStructure(entry.reference, snapshot).match(() => { }, (failure) => {
|
|
207
|
+
unresolved.push(failure);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (unresolved.length > 0) {
|
|
211
|
+
return err({ reason: "unresolved-reference", detail: `${unresolved.length} references do not resolve against the snapshot`, unresolved });
|
|
212
|
+
}
|
|
213
|
+
return ok(candidate);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Find a block by its id, with the path from the root. The search visits each block one time.
|
|
217
|
+
*
|
|
218
|
+
* The read surface needs the same search, thus this is the one find-by-id in the draft model.
|
|
219
|
+
*/
|
|
220
|
+
export function locate(document, id) {
|
|
221
|
+
return locateInBlocks(document.sections, id, [], undefined);
|
|
222
|
+
}
|
|
223
|
+
function locateInBlocks(blocks, id, prefix, parent) {
|
|
224
|
+
for (let index = 0; index < blocks.length; index++) {
|
|
225
|
+
const block = blocks[index];
|
|
226
|
+
const path = [...prefix, index];
|
|
227
|
+
if (block.id === id) {
|
|
228
|
+
return parent === undefined ? { block, path } : { block, path, parent };
|
|
229
|
+
}
|
|
230
|
+
if (block.kind === "section") {
|
|
231
|
+
const found = locateInBlocks(block.blocks, id, path, block);
|
|
232
|
+
if (found !== undefined) {
|
|
233
|
+
return found;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
/** Give the id of the block at a path, or `undefined` for the root. */
|
|
240
|
+
function idAtPath(document, path) {
|
|
241
|
+
if (path.length === 0) {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
let blocks = document.sections;
|
|
245
|
+
let block;
|
|
246
|
+
for (const index of path) {
|
|
247
|
+
block = blocks[index];
|
|
248
|
+
if (block !== undefined && block.kind === "section") {
|
|
249
|
+
blocks = block.blocks;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return block?.id;
|
|
253
|
+
}
|
|
254
|
+
/** Give the children array at a container path. The root path gives the sections of the document. */
|
|
255
|
+
function getChildren(document, containerPath) {
|
|
256
|
+
let blocks = document.sections;
|
|
257
|
+
for (const index of containerPath) {
|
|
258
|
+
const node = blocks[index];
|
|
259
|
+
if (node.kind !== "section") {
|
|
260
|
+
// A container path names a section at each step, thus this branch never runs.
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
blocks = node.blocks;
|
|
264
|
+
}
|
|
265
|
+
return blocks;
|
|
266
|
+
}
|
|
267
|
+
/** Rebuild the document with new children at a container path, and share each untouched branch. */
|
|
268
|
+
function setChildren(document, containerPath, children) {
|
|
269
|
+
if (containerPath.length === 0) {
|
|
270
|
+
// The root holds sections only. Each operation enforces that rule before it builds a candidate.
|
|
271
|
+
return { ...document, sections: children };
|
|
272
|
+
}
|
|
273
|
+
return { ...document, sections: setChildrenInBlocks(document.sections, containerPath, children) };
|
|
274
|
+
}
|
|
275
|
+
function setChildrenInBlocks(blocks, containerPath, children) {
|
|
276
|
+
const head = containerPath[0];
|
|
277
|
+
const rest = containerPath.slice(1);
|
|
278
|
+
const target = blocks[head];
|
|
279
|
+
if (target.kind !== "section") {
|
|
280
|
+
// A container path names a section at each step, thus this branch never runs.
|
|
281
|
+
return blocks;
|
|
282
|
+
}
|
|
283
|
+
const updated = rest.length === 0 ? { ...target, blocks: children } : { ...target, blocks: setChildrenInBlocks(target.blocks, rest, children) };
|
|
284
|
+
const copy = blocks.slice();
|
|
285
|
+
copy[head] = updated;
|
|
286
|
+
return copy;
|
|
287
|
+
}
|
|
288
|
+
/** Insert a block at an index inside a container. */
|
|
289
|
+
function insertAt(document, containerPath, index, block) {
|
|
290
|
+
const children = getChildren(document, containerPath);
|
|
291
|
+
return setChildren(document, containerPath, [...children.slice(0, index), block, ...children.slice(index)]);
|
|
292
|
+
}
|
|
293
|
+
/** Remove the block at an index inside a container. */
|
|
294
|
+
function removeAt(document, containerPath, index) {
|
|
295
|
+
const children = getChildren(document, containerPath);
|
|
296
|
+
return setChildren(document, containerPath, [...children.slice(0, index), ...children.slice(index + 1)]);
|
|
297
|
+
}
|
|
298
|
+
/** Replace the block at an index inside a container. */
|
|
299
|
+
function replaceAt(document, containerPath, index, block) {
|
|
300
|
+
const children = getChildren(document, containerPath);
|
|
301
|
+
const next = children.slice();
|
|
302
|
+
next[index] = block;
|
|
303
|
+
return setChildren(document, containerPath, next);
|
|
304
|
+
}
|
|
305
|
+
/** Count how many blocks of the document hold each id. */
|
|
306
|
+
function idCounts(document) {
|
|
307
|
+
const ids = [];
|
|
308
|
+
collectIds(document.sections, ids);
|
|
309
|
+
const counts = new Map();
|
|
310
|
+
for (const id of ids) {
|
|
311
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
312
|
+
}
|
|
313
|
+
return counts;
|
|
314
|
+
}
|
|
315
|
+
function collectIds(blocks, into) {
|
|
316
|
+
for (const block of blocks) {
|
|
317
|
+
into.push(block.id);
|
|
318
|
+
if (block.kind === "section") {
|
|
319
|
+
collectIds(block.blocks, into);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/** Give `true` when `prefix` is a prefix of `path`, or equal to it. */
|
|
324
|
+
function isPrefix(prefix, path) {
|
|
325
|
+
if (prefix.length > path.length) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
for (let index = 0; index < prefix.length; index++) {
|
|
329
|
+
if (prefix[index] !== path[index]) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
/** Reduce a parse failure to a `malformed-block` refusal that names the first fault. */
|
|
336
|
+
function malformed(error) {
|
|
337
|
+
const issue = error.issues[0];
|
|
338
|
+
const location = issue.path.map((segment) => String(segment)).join(".");
|
|
339
|
+
const detail = location.length > 0 ? `${location}: ${issue.message}` : issue.message;
|
|
340
|
+
return { reason: "malformed-block", detail };
|
|
341
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read-back surface of a draft.
|
|
3
|
+
*
|
|
4
|
+
* The agent holds the outline as its primary view. The outline gives one entry for each block: the id,
|
|
5
|
+
* the kind, the nesting depth, and a short label. It carries no binding and no full prose. Thus the full
|
|
6
|
+
* tree stays out of the agent context.
|
|
7
|
+
*
|
|
8
|
+
* A read of one block by its id gives the full block. The agent pulls a block only when it needs one. A
|
|
9
|
+
* section gives its own fields and the ids of its children, and never the subtree. The outline already
|
|
10
|
+
* names every descendant, thus a subtree here would return the whole tree that the outline exists to keep
|
|
11
|
+
* out.
|
|
12
|
+
*/
|
|
13
|
+
import type { AtomBlock } from "../contracts/report-blocks.js";
|
|
14
|
+
import type { DraftBlock, DraftDocument } from "./draft.js";
|
|
15
|
+
/**
|
|
16
|
+
* One outline entry. It carries the id, the kind, the nesting depth, and a short label. A top-level
|
|
17
|
+
* section is depth 0. The entry carries no binding, thus the outline stays small.
|
|
18
|
+
*/
|
|
19
|
+
export interface OutlineEntry {
|
|
20
|
+
id: string;
|
|
21
|
+
kind: DraftBlock["kind"];
|
|
22
|
+
depth: number;
|
|
23
|
+
label: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A section as the read surface gives it: its own fields, and the id of each child in document order.
|
|
27
|
+
*
|
|
28
|
+
* The children stay out. A section carries no field that the outline does not already give, thus the
|
|
29
|
+
* subtree would be a second copy of the outline, and a read of a top-level section would return the whole
|
|
30
|
+
* draft.
|
|
31
|
+
*/
|
|
32
|
+
export interface ShallowSection {
|
|
33
|
+
kind: "section";
|
|
34
|
+
id: string;
|
|
35
|
+
title: string;
|
|
36
|
+
childIds: string[];
|
|
37
|
+
}
|
|
38
|
+
/** One block as the read surface gives it. An atom reads in full, and a section reads shallow. */
|
|
39
|
+
export type ReadableBlock = AtomBlock | ShallowSection;
|
|
40
|
+
/**
|
|
41
|
+
* Build the outline of a draft. The walk is pre-order, thus the entries follow the document order. A
|
|
42
|
+
* section comes before its children, and a child sits one depth deeper than its parent.
|
|
43
|
+
*/
|
|
44
|
+
export declare function buildOutline(draft: DraftDocument): OutlineEntry[];
|
|
45
|
+
/**
|
|
46
|
+
* Build the outline of one container: an entry for each direct child, and nothing deeper.
|
|
47
|
+
*
|
|
48
|
+
* A landed operation reports the container that it changed, and the whole outline would grow the context
|
|
49
|
+
* by the size of the draft on every call. The child order is what the agent cannot know on its own, and a
|
|
50
|
+
* descendant did not move, thus one level answers the question that a landing raises.
|
|
51
|
+
*
|
|
52
|
+
* `depth` is the depth of the children, thus the entries read the same as the entries of a full outline.
|
|
53
|
+
*/
|
|
54
|
+
export declare function childOutline(blocks: readonly DraftBlock[], depth: number): OutlineEntry[];
|
|
55
|
+
/**
|
|
56
|
+
* Read one block by its id. An atom gives the block as it is, with its bindings. A section gives its own
|
|
57
|
+
* fields and the id of each child. A block that no id holds gives `undefined`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function readBlock(draft: DraftDocument, id: string): ReadableBlock | undefined;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read-back surface of a draft.
|
|
3
|
+
*
|
|
4
|
+
* The agent holds the outline as its primary view. The outline gives one entry for each block: the id,
|
|
5
|
+
* the kind, the nesting depth, and a short label. It carries no binding and no full prose. Thus the full
|
|
6
|
+
* tree stays out of the agent context.
|
|
7
|
+
*
|
|
8
|
+
* A read of one block by its id gives the full block. The agent pulls a block only when it needs one. A
|
|
9
|
+
* section gives its own fields and the ids of its children, and never the subtree. The outline already
|
|
10
|
+
* names every descendant, thus a subtree here would return the whole tree that the outline exists to keep
|
|
11
|
+
* out.
|
|
12
|
+
*/
|
|
13
|
+
import { locate } from "./draft-operations.js";
|
|
14
|
+
/** The label keeps a prose head of at most this many code points. A clipped label ends with the marker. */
|
|
15
|
+
const PROSE_CLIP = 80;
|
|
16
|
+
/** The marker that a clipped label ends with. It counts toward `PROSE_CLIP`. */
|
|
17
|
+
const CLIP_MARKER = "…";
|
|
18
|
+
/**
|
|
19
|
+
* Clip a label to `PROSE_CLIP` code points, and mark a clipped label.
|
|
20
|
+
*
|
|
21
|
+
* The cut counts code points and not UTF-16 units, because a cut at a fixed index splits a surrogate pair
|
|
22
|
+
* whenever an astral character straddles the boundary. A lone surrogate has no UTF-8 encoding, thus the
|
|
23
|
+
* agent would read a replacement character in place of the character the author wrote.
|
|
24
|
+
*
|
|
25
|
+
* A clipped label carries the marker, because a bare head and a whole short label read the same. Without
|
|
26
|
+
* the marker the agent cannot tell whether the prose ends there, and it acts on a truncated claim as if it
|
|
27
|
+
* were the whole one.
|
|
28
|
+
*/
|
|
29
|
+
function clip(text) {
|
|
30
|
+
const points = [...text];
|
|
31
|
+
if (points.length <= PROSE_CLIP) {
|
|
32
|
+
return text;
|
|
33
|
+
}
|
|
34
|
+
return `${points.slice(0, PROSE_CLIP - CLIP_MARKER.length).join("")}${CLIP_MARKER}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The short label of one block.
|
|
38
|
+
*
|
|
39
|
+
* A section, a metric, a table, and a chart give a titled name. A figure and a citation give a caption or
|
|
40
|
+
* a note. A text and a claim carry prose. Every one of them clips: a caption, a note, and a title are free
|
|
41
|
+
* prose too, thus an unclipped arm would put a paragraph into each outline that names the block.
|
|
42
|
+
*
|
|
43
|
+
* A block with an absent optional field gives an empty label.
|
|
44
|
+
*/
|
|
45
|
+
function labelOf(block) {
|
|
46
|
+
switch (block.kind) {
|
|
47
|
+
case "section":
|
|
48
|
+
return clip(block.title);
|
|
49
|
+
case "metric":
|
|
50
|
+
return clip(block.label);
|
|
51
|
+
case "table":
|
|
52
|
+
return clip(block.title ?? "");
|
|
53
|
+
case "chart":
|
|
54
|
+
return clip(block.title ?? "");
|
|
55
|
+
case "figure":
|
|
56
|
+
return clip(block.caption ?? "");
|
|
57
|
+
case "citation":
|
|
58
|
+
return clip(block.note ?? "");
|
|
59
|
+
case "text":
|
|
60
|
+
return clip(block.content.prose);
|
|
61
|
+
case "claim":
|
|
62
|
+
return clip(block.content.prose);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the outline of a draft. The walk is pre-order, thus the entries follow the document order. A
|
|
67
|
+
* section comes before its children, and a child sits one depth deeper than its parent.
|
|
68
|
+
*/
|
|
69
|
+
export function buildOutline(draft) {
|
|
70
|
+
const entries = [];
|
|
71
|
+
const visit = (block, depth) => {
|
|
72
|
+
entries.push({ id: block.id, kind: block.kind, depth, label: labelOf(block) });
|
|
73
|
+
if (block.kind === "section") {
|
|
74
|
+
for (const child of block.blocks) {
|
|
75
|
+
visit(child, depth + 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
for (const section of draft.sections) {
|
|
80
|
+
visit(section, 0);
|
|
81
|
+
}
|
|
82
|
+
return entries;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build the outline of one container: an entry for each direct child, and nothing deeper.
|
|
86
|
+
*
|
|
87
|
+
* A landed operation reports the container that it changed, and the whole outline would grow the context
|
|
88
|
+
* by the size of the draft on every call. The child order is what the agent cannot know on its own, and a
|
|
89
|
+
* descendant did not move, thus one level answers the question that a landing raises.
|
|
90
|
+
*
|
|
91
|
+
* `depth` is the depth of the children, thus the entries read the same as the entries of a full outline.
|
|
92
|
+
*/
|
|
93
|
+
export function childOutline(blocks, depth) {
|
|
94
|
+
return blocks.map((block) => ({ id: block.id, kind: block.kind, depth, label: labelOf(block) }));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read one block by its id. An atom gives the block as it is, with its bindings. A section gives its own
|
|
98
|
+
* fields and the id of each child. A block that no id holds gives `undefined`.
|
|
99
|
+
*/
|
|
100
|
+
export function readBlock(draft, id) {
|
|
101
|
+
const found = locate(draft, id)?.block;
|
|
102
|
+
if (found === undefined) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
if (found.kind === "section") {
|
|
106
|
+
return { kind: "section", id: found.id, title: found.title, childIds: found.blocks.map((child) => child.id) };
|
|
107
|
+
}
|
|
108
|
+
return found;
|
|
109
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The draft grammar of a report document under composition.
|
|
3
|
+
*
|
|
4
|
+
* An edit refuses what is wrong, and the finish refuses what is missing. Thus a draft permits emptiness,
|
|
5
|
+
* and the contract does not. The draft admits an empty section list and an empty section, and the
|
|
6
|
+
* completeness rules of `ReportDocumentSchema` gate one time, at the finish.
|
|
7
|
+
*
|
|
8
|
+
* The grammar composes from the contract atoms, and it does not copy them. The draft spreads
|
|
9
|
+
* `ATOM_BLOCK_SCHEMAS`, and the relaxed section is the only new part. Thus a ninth block kind reaches the
|
|
10
|
+
* draft the moment that the contract declares it, and the draft never drifts from the contract.
|
|
11
|
+
*
|
|
12
|
+
* The recursion mirrors `SectionBlockSchema`. The `blocks` field is a `z.lazy` and not a getter. Thus the
|
|
13
|
+
* discriminated union reads the `kind` discriminator at construction time, and it needs no reference to
|
|
14
|
+
* `DraftBlockSchema` before it exists. The relaxed rule holds at every depth, because a nested section is
|
|
15
|
+
* a `DraftSectionBlock` too.
|
|
16
|
+
*/
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { type AtomBlock } from "../contracts/report-blocks.js";
|
|
19
|
+
/**
|
|
20
|
+
* The draft section shape as a plain type. TypeScript cannot infer the block tree through the union
|
|
21
|
+
* cycle, thus the recursive members carry an explicit type. `DraftSectionBlock` and `DraftBlock` break
|
|
22
|
+
* the cycle.
|
|
23
|
+
*/
|
|
24
|
+
export interface DraftSectionBlock {
|
|
25
|
+
kind: "section";
|
|
26
|
+
id: string;
|
|
27
|
+
title: string;
|
|
28
|
+
blocks: DraftBlock[];
|
|
29
|
+
}
|
|
30
|
+
/** One draft block of any of the eight kinds. The atoms keep the contract shape, and only the section relaxes. */
|
|
31
|
+
export type DraftBlock = AtomBlock | DraftSectionBlock;
|
|
32
|
+
/** A draft section permits zero child blocks, thus an empty section is a legal draft state. */
|
|
33
|
+
export declare const DraftSectionBlockSchema: z.ZodObject<{
|
|
34
|
+
kind: z.ZodLiteral<"section">;
|
|
35
|
+
id: z.ZodString;
|
|
36
|
+
title: z.ZodString;
|
|
37
|
+
blocks: z.ZodLazy<z.ZodArray<z.ZodType<DraftBlock, unknown, z.core.$ZodTypeInternals<DraftBlock, unknown>>>>;
|
|
38
|
+
}, z.core.$strict>;
|
|
39
|
+
/** The draft block union. A `z.ZodType<DraftBlock>` annotation gives the recursive members a stable type. */
|
|
40
|
+
export declare const DraftBlockSchema: z.ZodType<DraftBlock>;
|
|
41
|
+
/** The root of a draft. It permits zero sections, thus an empty draft is a legal state. */
|
|
42
|
+
export declare const DraftDocumentSchema: z.ZodObject<{
|
|
43
|
+
title: z.ZodString;
|
|
44
|
+
sections: z.ZodArray<z.ZodObject<{
|
|
45
|
+
kind: z.ZodLiteral<"section">;
|
|
46
|
+
id: z.ZodString;
|
|
47
|
+
title: z.ZodString;
|
|
48
|
+
blocks: z.ZodLazy<z.ZodArray<z.ZodType<DraftBlock, unknown, z.core.$ZodTypeInternals<DraftBlock, unknown>>>>;
|
|
49
|
+
}, z.core.$strict>>;
|
|
50
|
+
}, z.core.$strict>;
|
|
51
|
+
export type DraftDocument = z.infer<typeof DraftDocumentSchema>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The draft grammar of a report document under composition.
|
|
3
|
+
*
|
|
4
|
+
* An edit refuses what is wrong, and the finish refuses what is missing. Thus a draft permits emptiness,
|
|
5
|
+
* and the contract does not. The draft admits an empty section list and an empty section, and the
|
|
6
|
+
* completeness rules of `ReportDocumentSchema` gate one time, at the finish.
|
|
7
|
+
*
|
|
8
|
+
* The grammar composes from the contract atoms, and it does not copy them. The draft spreads
|
|
9
|
+
* `ATOM_BLOCK_SCHEMAS`, and the relaxed section is the only new part. Thus a ninth block kind reaches the
|
|
10
|
+
* draft the moment that the contract declares it, and the draft never drifts from the contract.
|
|
11
|
+
*
|
|
12
|
+
* The recursion mirrors `SectionBlockSchema`. The `blocks` field is a `z.lazy` and not a getter. Thus the
|
|
13
|
+
* discriminated union reads the `kind` discriminator at construction time, and it needs no reference to
|
|
14
|
+
* `DraftBlockSchema` before it exists. The relaxed rule holds at every depth, because a nested section is
|
|
15
|
+
* a `DraftSectionBlock` too.
|
|
16
|
+
*/
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { ATOM_BLOCK_SCHEMAS } from "../contracts/report-blocks.js";
|
|
19
|
+
/** A draft section permits zero child blocks, thus an empty section is a legal draft state. */
|
|
20
|
+
export const DraftSectionBlockSchema = z.strictObject({
|
|
21
|
+
kind: z.literal("section"),
|
|
22
|
+
id: z.string().min(1),
|
|
23
|
+
title: z.string(),
|
|
24
|
+
blocks: z.lazy(() => z.array(DraftBlockSchema)),
|
|
25
|
+
});
|
|
26
|
+
/** The draft block union. A `z.ZodType<DraftBlock>` annotation gives the recursive members a stable type. */
|
|
27
|
+
export const DraftBlockSchema = z.discriminatedUnion("kind", [DraftSectionBlockSchema, ...ATOM_BLOCK_SCHEMAS]);
|
|
28
|
+
/** The root of a draft. It permits zero sections, thus an empty draft is a legal state. */
|
|
29
|
+
export const DraftDocumentSchema = z.strictObject({
|
|
30
|
+
title: z.string(),
|
|
31
|
+
sections: z.array(DraftSectionBlockSchema),
|
|
32
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A local, in-memory realization of the reference resolver. It reads only the snapshot that it is given,
|
|
3
|
+
* thus it needs no storage and no network. A test and a benchmark use it to exercise the resolution and
|
|
4
|
+
* assertion rules without a host.
|
|
5
|
+
*/
|
|
6
|
+
import { type ReferenceResolver } from "./reference-resolver.js";
|
|
7
|
+
/** Make a resolver that reads only the snapshot. It holds no state, thus one instance serves every call. */
|
|
8
|
+
export declare function createFixtureResolver(): ReferenceResolver;
|