@ai-markdown/core 3.0.0 → 3.0.1
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/README.md +20 -18
- package/dist/index.cjs +102 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.dev.cjs +102 -33
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +106 -36
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +106 -36
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @ai-markdown/core
|
|
2
2
|
|
|
3
|
+
[Documentation](https://ai-markdown.github.io/docs/core/) · [Examples](https://ai-markdown.github.io/examples/) · [Website](https://ai-markdown.github.io/)
|
|
4
|
+
|
|
3
5
|
[](https://www.npmjs.com/package/@ai-markdown/core?activeTab=versions)
|
|
4
6
|
[](https://www.npmjs.com/package/@ai-markdown/core)
|
|
5
7
|
[](https://github.com/ai-markdown/ai-markdown/tree/main/packages/core)
|
|
@@ -7,7 +9,7 @@
|
|
|
7
9
|
|
|
8
10
|
Framework-independent orchestration for ai-markdown adapters, on the **3.0.0** train alongside the React and Vue adapters. It supplies pipeline sessions, block planning, cross-chunk contributions, aggregate footnote trees and streaming coordination. Applications install `@ai-markdown/react` or `@ai-markdown/vue`; adapter authors can install `@ai-markdown/core` and `@ai-markdown/engine` directly.
|
|
9
11
|
|
|
10
|
-
This package was the private runtime in the legacy v2.14.1 release. It is now a real external dependency of both framework adapters, with an explicit public export list. The old `@ai-react-markdown/core` React package maps to `@ai-markdown/react`, not this package. See the [migration guide](
|
|
12
|
+
This package was the private runtime in the legacy v2.14.1 release. It is now a real external dependency of both framework adapters, with an explicit public export list. The old `@ai-react-markdown/core` React package maps to `@ai-markdown/react`, not this package. See the [migration guide](https://ai-markdown.github.io/docs/guides/framework-transition/). Public contracts follow semantic versioning from 3.0.0; keep the shared packages at the same exact train version.
|
|
11
13
|
|
|
12
14
|
## Responsibility and dependency direction
|
|
13
15
|
|
|
@@ -20,17 +22,17 @@ This package was the private runtime in the legacy v2.14.1 release. It is now a
|
|
|
20
22
|
|
|
21
23
|
The adapter may also consume engine primitives directly. Shared core does not duplicate the engine's public barrel or wrap every engine function merely to rename it. It owns reusable orchestration that would otherwise have to be copied into another framework adapter.
|
|
22
24
|
|
|
23
|
-
| Module | Responsibility
|
|
24
|
-
| ---------------------------- |
|
|
25
|
-
| `coordinationPreparation.ts` | Derive phantom targets, select coordinated/orphan handlers and harvesting policy, build contribution invalidation tuples
|
|
26
|
-
| `pipelineSession.ts` | Merge coordinated parse options, append protected phantom suffixes, select full/incremental parsing, invalidate failed state
|
|
27
|
-
| `blockPlan.ts` | Associate transformed HAST with source MDAST, produce stable keys, classify reference dependencies and swallowed HTML, compute registry fingerprints
|
|
28
|
-
| `blockPlanner.ts` | Reuse eligible retained prefix plans while preserving whole-document reference context
|
|
29
|
-
| `contribution.ts` | Compare source and policy fingerprints, harvest transformed definition bodies, publish changed committed contributions
|
|
30
|
-
| `aggregateFootnotes.ts` | Assemble a document's ordered footnotes and occurrence backrefs as HAST
|
|
31
|
-
| `cloneHastForRender.ts` | Clone node/children/properties/data and original-URL containers before render-time mutation
|
|
32
|
-
| `smoothCoordinator.ts` | Order chunk reveal, maintain sticky completion, defer cleanup and coalesce notifications
|
|
33
|
-
| `tailSignal.ts` | Classify a source tail that renders inside a footnote or as an invisible link definition
|
|
25
|
+
| Module | Responsibility | State owner |
|
|
26
|
+
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
27
|
+
| `coordinationPreparation.ts` | Derive phantom targets, select coordinated/orphan handlers and harvesting policy, build contribution invalidation tuples | Pure decisions; previous snapshots belong to the caller |
|
|
28
|
+
| `pipelineSession.ts` | Merge coordinated parse options, append protected phantom suffixes, select full/incremental parsing, invalidate failed state, fall back to a full parse or, on the engine's raw-depth error only, to a plain-text frame | One session per rendered chunk |
|
|
29
|
+
| `blockPlan.ts` | Associate transformed HAST with source MDAST, produce stable keys, classify reference dependencies and swallowed HTML, compute registry fingerprints | Pure functions over the supplied trees and registry |
|
|
30
|
+
| `blockPlanner.ts` | Reuse eligible retained prefix plans while preserving whole-document reference context | One planner per rendered chunk |
|
|
31
|
+
| `contribution.ts` | Compare source and policy fingerprints, harvest transformed definition bodies, publish changed committed contributions | One publisher per mounted chunk |
|
|
32
|
+
| `aggregateFootnotes.ts` | Assemble a document's ordered footnotes and occurrence backrefs as HAST | Returned tree belongs to the caller |
|
|
33
|
+
| `cloneHastForRender.ts` | Clone node/children/properties/data and original-URL containers before render-time mutation | Returned clone belongs to the caller |
|
|
34
|
+
| `smoothCoordinator.ts` | Order chunk reveal, maintain sticky completion, defer cleanup and coalesce notifications | One coordinator per logical document |
|
|
35
|
+
| `tailSignal.ts` | Classify a source tail that renders inside a footnote or as an invisible link definition | Pure function over MDAST |
|
|
34
36
|
|
|
35
37
|
The engine remains responsible for grammar, preprocessing, plugin chains, scanners, incremental parsing algorithms, registry storage/indexing, reference resolution and URL policy primitives. Shared core uses those algorithms to implement reusable rendering decisions.
|
|
36
38
|
|
|
@@ -77,7 +79,7 @@ This is a standalone example using the public package entries. A coordinated ada
|
|
|
77
79
|
|
|
78
80
|
The host chooses `incrementalParse: false` for a one-shot server render. Shared core does not inspect `window` to infer the host. A later incremental frame starts from fresh state after a non-incremental frame. Call `reset()` when the host invalidates retained parse state because render policy changed. Parse-input identities are also checked by the engine's dependency key; phantom suffix changes remain always-tail input rather than invalidating all retained source.
|
|
79
81
|
|
|
80
|
-
An incremental failure clears the retained state before retrying the full pipeline.
|
|
82
|
+
An incremental failure clears the retained state before retrying the full pipeline. The full pipeline has one degraded outcome: when the engine's raw-HTML step reports `EngineRawHtmlDepthError` (element nesting deeper than the engine's bound, `RAW_HTML_MAX_DEPTH`, which hundreds of nested raw `<div>` tags exceed), the session logs the error in development builds and returns the frame as one plain-text paragraph whose positions cover the whole source, so one hostile message cannot take the host surface down; the next frame parses normally. That last resort renders escaped text, not a stale frame. Every other exception from the full pipeline propagates to the host, including a `RangeError` that is not that overflow and any throw from a remark or rehype plugin or handler the host supplied: those are bugs to surface, not frames to render as text, and the fallback is not a substitute for an error boundary around adapter code. Each session and planner is mutable local computation state: do not share one across independent chunks or concurrently executing consumers.
|
|
81
83
|
|
|
82
84
|
## Shared preparation decisions
|
|
83
85
|
|
|
@@ -89,7 +91,7 @@ An incremental failure clears the retained state before retrying the full pipeli
|
|
|
89
91
|
|
|
90
92
|
These functions do not register, subscribe, publish or mutate their supplied snapshots. Treat label sets and returned objects as immutable; pass a previous result only from the same logical consumer. React retains the previous snapshots in refs and uses memoization; Vue uses local variables behind computed values. The engine and planner still own correctness when a source is replaced or a retained render is discarded.
|
|
91
93
|
|
|
92
|
-
The published [Vue adapter](../vue/README.md) uses these decisions in its component lifecycle while keeping AST/registry objects out of deep reactive proxies. It has SSR, hydration, slots, base styles and cursor support, with
|
|
94
|
+
The published [Vue adapter](../vue/README.md) uses these decisions in its component lifecycle while keeping AST/registry objects out of deep reactive proxies. It has SSR, hydration, slots, base styles and cursor support, with functional adapter checks in Chromium, Firefox and WebKit, and forced-GC lifecycle checks in Chromium. The earlier private prototype is archived under `prototypes/` and is not the application entry point.
|
|
93
95
|
|
|
94
96
|
## Preparation and commit have different effects
|
|
95
97
|
|
|
@@ -124,16 +126,16 @@ pnpm preflight
|
|
|
124
126
|
|
|
125
127
|
Build before running distribution tests: they load the actual ESM/CJS production and development files in separate Node processes, reject UI framework resolution transitively and parse without browser globals. Other tests compare session output with an independent full engine pipeline, exercise resets/fallback, prove explicit contribution timing and check aggregate body immutability. Existing React tests still exercise the adapter's delegation, memoization, byte equivalence, coordination and browser behavior.
|
|
126
128
|
|
|
127
|
-
The dedicated `test:core-contracts` gate builds core and its workspace dependencies, typechecks it, and runs all core tests independently of React/Vue. It runs in preflight, a dedicated CI task and the release workflow. Pure planner, coordinator, tail-signal and block-plan tests live with core; React retains renderer/cache integration tests. Fixed-seed sequences compare retained pipeline/planning and contribution state against full reconstruction, and coordinator lifecycles against a separate model. See [core testing](
|
|
129
|
+
The dedicated `test:core-contracts` gate builds core and its workspace dependencies, typechecks it, and runs all core tests independently of React/Vue. It runs in preflight, a dedicated CI task and the release workflow. Pure planner, coordinator, tail-signal and block-plan tests live with core; React retains renderer/cache integration tests. Fixed-seed sequences compare retained pipeline/planning and contribution state against full reconstruction, and coordinator lifecycles against a separate model. See [core testing](https://ai-markdown.github.io/docs/guides/core-testing/) for the module map, fixed budgets, replay commands and limitations.
|
|
128
130
|
|
|
129
131
|
`assert-boundary.mjs` checks public-package identity, allowed production dependencies, source import direction and folded environment gates. The React distribution guard requires external core and engine imports. Core's declarations must not expose `RegistryInternal`, `SmoothCoordinatorInternal` or private refcount/subscriber containers.
|
|
130
132
|
|
|
131
|
-
The release train is engine/core/react/react-mantine/vue at the same version. Vue now consumes the same shared contracts; see the [API contracts](
|
|
133
|
+
The release train is engine/core/react/react-mantine/vue at the same version. Vue now consumes the same shared contracts; see the [API contracts](https://ai-markdown.github.io/docs/guides/api/core-engine-contracts/) for stable signatures and migration from beta.1. Core depends on engine through `workspace:*`, which becomes the exact train version in the published manifest. Both ESM and CJS have production and development entries; every build folds environment gates separately. Core has no `use client` directive and does not inline a second engine implementation.
|
|
132
134
|
|
|
133
135
|
## Public API and write capabilities
|
|
134
136
|
|
|
135
|
-
The root entry explicitly lists supported
|
|
137
|
+
The root entry explicitly lists supported stable exports. Session creation, parsing, planning, pure coordination preparation, contribution commit, aggregate construction, structural cloning and source-tail classification are adapter contracts. The existing block digest and fingerprint functions remain available because React consumes them; their source/ownership assumptions above still apply.
|
|
136
138
|
|
|
137
139
|
`createSmoothCoordinator` returns the read-only state and documented methods of `SmoothCoordinator`, excluding internal refcounts and notification containers. `ContributionOptions.registry` accepts only a `ContributionRegistry` write capability with `contributeChunkData`; it does not require an implementation registry. Create a registry through engine's `createRegistry`, whose `RegistryController` adds registration and publication to the read-only `Registry` contract. Pair every registration with release in the owning adapter.
|
|
138
140
|
|
|
139
|
-
These are type-level API boundaries, not object freezing or arbitrary deep cloning. Mutating returned sets, tree data or undocumented implementation fields is unsupported. Public contracts and checked signatures are recorded in the [API contract document](
|
|
141
|
+
These are type-level API boundaries, not object freezing or arbitrary deep cloning. Mutating returned sets, tree data or undocumented implementation fields is unsupported. Public contracts and checked signatures are recorded in the [API contract document](https://ai-markdown.github.io/docs/guides/api/core-engine-contracts/).
|
package/dist/index.cjs
CHANGED
|
@@ -383,19 +383,49 @@ function createBlockPlanner() {
|
|
|
383
383
|
let h = 0;
|
|
384
384
|
let reusedContext = false;
|
|
385
385
|
if (previous && source.startsWith(previous.source) && options.phantomFootnoteLabels === previous.phantoms) {
|
|
386
|
+
const prevHast = previous.hast.children;
|
|
387
|
+
const prevPlan = previous.result.plan;
|
|
388
|
+
const isRetainedGap = (i) => {
|
|
389
|
+
const gap = hast.children[i];
|
|
390
|
+
return gap === prevHast[i] && gap.type === "text" && !/\S/.test(gap.value);
|
|
391
|
+
};
|
|
386
392
|
while (m < mdast.children.length) {
|
|
387
393
|
const node = mdast.children[m];
|
|
388
394
|
const el = hast.children[h];
|
|
389
|
-
const item =
|
|
390
|
-
if (node !== previous.mdast.children[m] || el !==
|
|
395
|
+
const item = prevPlan[h];
|
|
396
|
+
if (node !== previous.mdast.children[m] || el !== prevHast[h] || !el || el.type !== "element" || !item || item.kind !== "block" || item.info.startOffset !== node.position?.start.offset || !isEligible(node))
|
|
391
397
|
break;
|
|
398
|
+
const startOffset = item.info.startOffset;
|
|
399
|
+
let next = h + 1;
|
|
400
|
+
let complete = true;
|
|
401
|
+
for (; ; ) {
|
|
402
|
+
let probe = next;
|
|
403
|
+
while (probe < hast.children.length && isRetainedGap(probe)) probe++;
|
|
404
|
+
const sibling = prevPlan[probe];
|
|
405
|
+
if (!sibling || sibling.kind !== "block" || sibling.info.startOffset !== startOffset) break;
|
|
406
|
+
if (hast.children[probe] !== prevHast[probe]) {
|
|
407
|
+
complete = false;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
next = probe + 1;
|
|
411
|
+
}
|
|
412
|
+
if (!complete) break;
|
|
392
413
|
reusedContext ||= item.info.hasReference;
|
|
393
414
|
m++;
|
|
394
|
-
h
|
|
395
|
-
while (h < hast.children.length)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
415
|
+
h = next;
|
|
416
|
+
while (h < hast.children.length && isRetainedGap(h)) h++;
|
|
417
|
+
}
|
|
418
|
+
if (m > 0) {
|
|
419
|
+
const prefixEnd = mdast.children[m - 1].position?.end.offset ?? 0;
|
|
420
|
+
for (let i = h; i < hast.children.length; i++) {
|
|
421
|
+
const child = hast.children[i];
|
|
422
|
+
const at = child.type === "element" ? child.position?.start.offset : void 0;
|
|
423
|
+
if (at !== void 0 && at < prefixEnd) {
|
|
424
|
+
m = 0;
|
|
425
|
+
h = 0;
|
|
426
|
+
reusedContext = false;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
399
429
|
}
|
|
400
430
|
}
|
|
401
431
|
}
|
|
@@ -442,7 +472,12 @@ function createContributionSession() {
|
|
|
442
472
|
phantomFootnoteLabels: targetPhantoms.missingFootnotes
|
|
443
473
|
})) {
|
|
444
474
|
if (node.kind === "ref") {
|
|
445
|
-
refs.push({
|
|
475
|
+
refs.push({
|
|
476
|
+
label: node.label,
|
|
477
|
+
kind: node.refKind,
|
|
478
|
+
referenceType: node.referenceType,
|
|
479
|
+
...node.nestedIn !== void 0 ? { nestedIn: node.nestedIn } : {}
|
|
480
|
+
});
|
|
446
481
|
} else if (node.kind === "fnDef") {
|
|
447
482
|
defMeta.set(node.label, {
|
|
448
483
|
identifier: node.label,
|
|
@@ -476,14 +511,14 @@ function createContributionSession() {
|
|
|
476
511
|
if (last?.registry === registry && last.symbol === sym && last.fp === fp && last.chain.length === chain.length && last.chain.every((dep, i) => dep === chain[i])) {
|
|
477
512
|
return;
|
|
478
513
|
}
|
|
479
|
-
const
|
|
514
|
+
const bodiesBySafeId = (0, import_engine2.extractDefBodiesFromHast)(pipeline.hast, clobberPrefix);
|
|
480
515
|
const defs = /* @__PURE__ */ new Map();
|
|
481
516
|
for (const [label, meta] of defMeta) {
|
|
482
517
|
defs.set(label, {
|
|
483
518
|
identifier: meta.identifier,
|
|
484
519
|
sourceIdentifier: meta.sourceIdentifier,
|
|
485
520
|
contentSource: meta.contentSource,
|
|
486
|
-
bodyHast:
|
|
521
|
+
bodyHast: bodiesBySafeId.get((0, import_engine2.footnoteSafeId)(meta.sourceIdentifier)) ?? []
|
|
487
522
|
});
|
|
488
523
|
}
|
|
489
524
|
lastContribution = { registry, symbol: sym, fp, chain };
|
|
@@ -691,6 +726,7 @@ function buildAggregateTree(registry, clobberPrefix, preserveOrphanReferences =
|
|
|
691
726
|
});
|
|
692
727
|
}
|
|
693
728
|
}
|
|
729
|
+
ordered.sort((a, b) => (a.n ?? 0) - (b.n ?? 0));
|
|
694
730
|
if (preserveOrphanReferences) {
|
|
695
731
|
for (const sym of registry.chunkOrder) {
|
|
696
732
|
const data = registry.chunkData.get(sym);
|
|
@@ -778,6 +814,30 @@ function buildAggregateTree(registry, clobberPrefix, preserveOrphanReferences =
|
|
|
778
814
|
// src/pipelineSession.ts
|
|
779
815
|
var import_engine4 = require("@ai-markdown/engine");
|
|
780
816
|
var unmeasured = (_stage, fn) => fn();
|
|
817
|
+
function endPoint(text) {
|
|
818
|
+
let line = 1;
|
|
819
|
+
let lineStart = 0;
|
|
820
|
+
for (let i = 0; i < text.length; i++) {
|
|
821
|
+
const code = text.charCodeAt(i);
|
|
822
|
+
if (code === 10 || code === 13 && text.charCodeAt(i + 1) !== 10) {
|
|
823
|
+
line += 1;
|
|
824
|
+
lineStart = i + 1;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return { line, column: text.length - lineStart + 1, offset: text.length };
|
|
828
|
+
}
|
|
829
|
+
function plainTextTrees(content) {
|
|
830
|
+
const position = { start: { line: 1, column: 1, offset: 0 }, end: endPoint(content) };
|
|
831
|
+
const text = { type: "text", value: content, position };
|
|
832
|
+
return {
|
|
833
|
+
mdast: { type: "root", children: [{ type: "paragraph", children: [text], position }], position },
|
|
834
|
+
hast: {
|
|
835
|
+
type: "root",
|
|
836
|
+
children: [{ type: "element", tagName: "p", properties: {}, children: [text], position }],
|
|
837
|
+
position
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
}
|
|
781
841
|
function createPipelineSession() {
|
|
782
842
|
let state = null;
|
|
783
843
|
return {
|
|
@@ -819,19 +879,33 @@ function createPipelineSession() {
|
|
|
819
879
|
} : {
|
|
820
880
|
...remarkRehypeOptions
|
|
821
881
|
};
|
|
882
|
+
const fullPipeline = () => {
|
|
883
|
+
try {
|
|
884
|
+
const parsed = measureHere(
|
|
885
|
+
"parse",
|
|
886
|
+
() => (0, import_engine4.parseStage)({
|
|
887
|
+
children: augmented,
|
|
888
|
+
remarkPlugins,
|
|
889
|
+
rehypePlugins,
|
|
890
|
+
remarkRehypeOptions: mergedRemarkRehypeOptions
|
|
891
|
+
})
|
|
892
|
+
);
|
|
893
|
+
const hastRoot = measureHere("transform", () => (0, import_engine4.transformStage)(parsed));
|
|
894
|
+
return { mdast: parsed.mdast, hast: hastRoot };
|
|
895
|
+
} catch (error) {
|
|
896
|
+
if (!(error instanceof import_engine4.EngineRawHtmlDepthError)) throw error;
|
|
897
|
+
if (false) {
|
|
898
|
+
console.error(
|
|
899
|
+
"[ai-react-markdown] raw HTML nested past the engine depth bound \u2014 rendering this frame as plain text:",
|
|
900
|
+
error
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
return plainTextTrees(content ?? "");
|
|
904
|
+
}
|
|
905
|
+
};
|
|
822
906
|
if (!incrementalParse) {
|
|
823
907
|
state = null;
|
|
824
|
-
|
|
825
|
-
"parse",
|
|
826
|
-
() => (0, import_engine4.parseStage)({
|
|
827
|
-
children: augmented,
|
|
828
|
-
remarkPlugins,
|
|
829
|
-
rehypePlugins,
|
|
830
|
-
remarkRehypeOptions: mergedRemarkRehypeOptions
|
|
831
|
-
})
|
|
832
|
-
);
|
|
833
|
-
const hastRoot = measureHere("transform", () => (0, import_engine4.transformStage)(parsed));
|
|
834
|
-
return { mdast: parsed.mdast, hast: hastRoot };
|
|
908
|
+
return fullPipeline();
|
|
835
909
|
}
|
|
836
910
|
try {
|
|
837
911
|
const result = (0, import_engine4.advanceIncrementalParse)(state, content ?? "", {
|
|
@@ -854,7 +928,12 @@ function createPipelineSession() {
|
|
|
854
928
|
// Explicit even though `rehypePlugins` already carries it: a
|
|
855
929
|
// credential change must never reuse trees stamped under the
|
|
856
930
|
// old one.
|
|
857
|
-
provenance
|
|
931
|
+
provenance,
|
|
932
|
+
// The boundary scanner's grammar profile. The scan checkpoint
|
|
933
|
+
// already refuses to resume under a different profile, but the
|
|
934
|
+
// retained TREES were still spliced against — a prefix frozen
|
|
935
|
+
// under one profile must not survive into frames under the other.
|
|
936
|
+
defListEnabled
|
|
858
937
|
],
|
|
859
938
|
defListEnabled,
|
|
860
939
|
phantomSuffix,
|
|
@@ -867,17 +946,7 @@ function createPipelineSession() {
|
|
|
867
946
|
if (false) {
|
|
868
947
|
console.error("[ai-react-markdown] incremental parse failed \u2014 full parse fallback for this frame:", error);
|
|
869
948
|
}
|
|
870
|
-
|
|
871
|
-
"parse",
|
|
872
|
-
() => (0, import_engine4.parseStage)({
|
|
873
|
-
children: augmented,
|
|
874
|
-
remarkPlugins,
|
|
875
|
-
rehypePlugins,
|
|
876
|
-
remarkRehypeOptions: mergedRemarkRehypeOptions
|
|
877
|
-
})
|
|
878
|
-
);
|
|
879
|
-
const hastRoot = measureHere("transform", () => (0, import_engine4.transformStage)(parsed));
|
|
880
|
-
return { mdast: parsed.mdast, hast: hastRoot };
|
|
949
|
+
return fullPipeline();
|
|
881
950
|
}
|
|
882
951
|
}
|
|
883
952
|
};
|