@sema-agent/core 7.1.0 → 7.2.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 +36 -0
- package/dist/agents/cross-session-envelope.d.ts +138 -0
- package/dist/agents/cross-session-envelope.js +191 -0
- package/dist/agents/cross-session-judge.d.ts +119 -0
- package/dist/agents/cross-session-judge.js +184 -0
- package/dist/agents/cross-session-ref.d.ts +52 -0
- package/dist/agents/cross-session-ref.js +64 -0
- package/dist/agents/send-message-tool.d.ts +13 -0
- package/dist/agents/send-message-tool.js +36 -12
- package/dist/core/checkpoint-store.d.ts +189 -3
- package/dist/core/checkpoint-store.js +56 -16
- package/dist/core/hooks.d.ts +15 -8
- package/dist/core/hooks.js +6 -3
- package/dist/core/permission-rule-consent.d.ts +72 -23
- package/dist/core/permission-rule-consent.js +115 -26
- package/dist/core/permission-rule-model.d.ts +245 -51
- package/dist/core/permission-rule-model.js +312 -54
- package/dist/core/permission-rule-org.js +13 -6
- package/dist/core/remote-env.d.ts +8 -1
- package/dist/core/runner/assemble-result.js +2 -1
- package/dist/core/runner/prepare-task.d.ts +39 -1
- package/dist/core/runner/prepare-task.js +278 -113
- package/dist/core/runner/prepare-workspace-restore.d.ts +6 -1
- package/dist/core/runner/prepare-workspace-restore.js +2 -1
- package/dist/core/runner/runtask.js +13 -3
- package/dist/core/task-notification.d.ts +64 -5
- package/dist/core/task-notification.js +25 -4
- package/dist/core/tool-policy.d.ts +11 -0
- package/dist/core/types.d.ts +23 -0
- package/dist/core/untrusted-text.js +17 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +125 -1
|
@@ -765,7 +765,12 @@ export type PendingAction = {
|
|
|
765
765
|
* order (redemption TICKETS are keyed on the consent record's flat CANDIDATE list instead —
|
|
766
766
|
* a chosen offer is redeemed via `redeemRuleBatch`), per-element parse with single-row
|
|
767
767
|
* degrade on an unknown `kind` and
|
|
768
|
-
* original-index preservation.
|
|
768
|
+
* original-index preservation. design/382 B3: a batch offer's members are the
|
|
769
|
+
* {@link import("./permission-rule-model.js").RuleOfferBatchMember} discriminated union
|
|
770
|
+
* (`command` | `directoryRead`), and the member-level degrade is the union's own normative
|
|
771
|
+
* arm — an unknown MEMBER kind drops the WHOLE batch offer (never one member: a conjunction
|
|
772
|
+
* silently one member short renders "yes to N" as "yes to N−1"), while the sibling single
|
|
773
|
+
* offers stay rendered. The park→resume redemption chain walks the same consent
|
|
769
774
|
* protocol as the synchronous card — there is no second form. */
|
|
770
775
|
ruleOffers?: readonly import("./permission-rule-model.js").RuleOffer[];
|
|
771
776
|
/** #490 修② (additive; no checkpoint-version bump — the `previewWithheld`/`hasBidiControls`
|
|
@@ -777,7 +782,11 @@ export type PendingAction = {
|
|
|
777
782
|
* the tool's own mandate marks, a hook-raised ask, an ancestor's authority, or — #502 — a
|
|
778
783
|
* demotion this CALL's reversibility probe declared structural, which for the built-in shell
|
|
779
784
|
* probe means a read outside the directories the session declared; an inbox must NOT point at
|
|
780
|
-
* rule-writing on this arm
|
|
785
|
+
* rule-writing on this arm — "allow rules silence the classifier's questions, never a mandated
|
|
786
|
+
* one" — nor at a directory grant: the subpath-rule family exists, but the out-of-root arm has
|
|
787
|
+
* no clearing configuration in this version, so a grant minted in answer to this line would
|
|
788
|
+
* never take effect where it was minted; confirming the call is the whole of what a person can
|
|
789
|
+
* do about it),
|
|
781
790
|
* `"shadowed"` (a rule the person already wrote is
|
|
782
791
|
* speaking and does not clear it), `"lane_cannot_speak"` (this card has no rule to offer — the
|
|
783
792
|
* grammar has no text for the command, or the card's array-order contract declines to present
|
|
@@ -786,6 +795,14 @@ export type PendingAction = {
|
|
|
786
795
|
* "there are offers" and the structural doors (no lane wired, another tool, a task that cannot
|
|
787
796
|
* hold a rule) — so read presence. See the synchronous seat's doc for the full contract. */
|
|
788
797
|
ruleOffersAbsence?: "mandated" | "lane_cannot_speak" | "shadowed";
|
|
798
|
+
/** design/382 §2.4 (adversarial-review r3; additive — no checkpoint-version bump, the
|
|
799
|
+
* `previewWithheld` precedent: an optional field an older reader ignores and the resume
|
|
800
|
+
* path never reads) — the PARK twin of `AskRequest.execCwd`, minted by the SAME factory as
|
|
801
|
+
* {@link ruleOffers}: the relative-cd resolution base those offers were minted with (the
|
|
802
|
+
* live tracked cwd at park time). An approval inbox reconstructing the authoritative
|
|
803
|
+
* consent record threads it as `prepareCardApproval`'s `execCwd`. Present only beside
|
|
804
|
+
* {@link ruleOffers} when the run had a tracked cwd; echo-only, never a control input. */
|
|
805
|
+
execCwd?: string;
|
|
789
806
|
/**
|
|
790
807
|
* design/80 D-1 §2 (slice 1a.2): the server-minted **opaque** boundInputHash of {@link args} — a
|
|
791
808
|
* SHA-256 (hex) via {@link import("./canonical-json.js").boundInputHashOf}, computed ONCE here at
|
|
@@ -1483,6 +1500,49 @@ export interface ResolveExpectation {
|
|
|
1483
1500
|
/** The monotonic {@link Checkpoint.rev} the caller observed at `get()` (absent rev ⇒ legacy `0`). */
|
|
1484
1501
|
rev: number;
|
|
1485
1502
|
}
|
|
1503
|
+
/** design/384 slice 2 / S-25-R1 — the terminal intents a claim can carry. Deliberately the
|
|
1504
|
+
* TERMINAL pair only: `resolve` and `expire` are the two verbs that race one pending row to
|
|
1505
|
+
* its end (the design/51 fence law). `reopen` (resolved→pending) is not terminal and `reap`
|
|
1506
|
+
* is a bulk sweep — both stay on their own verbs. */
|
|
1507
|
+
export type TerminalClaimIntent = {
|
|
1508
|
+
kind: "resolve";
|
|
1509
|
+
outcome: ResumeOutcome;
|
|
1510
|
+
expect?: ResolveExpectation;
|
|
1511
|
+
} | {
|
|
1512
|
+
kind: "expire";
|
|
1513
|
+
};
|
|
1514
|
+
/** design/384 slice 2 / S-25-R1 — the single-round-trip answer: EITHER the claim won, OR it lost and
|
|
1515
|
+
* the SAME atomic operation reports what the row is now. `current` reuses the row's own vocabulary
|
|
1516
|
+
* (status / rev / resolvedOutcome) — no new value domain. `status:"pending"` on a loss is the OCC
|
|
1517
|
+
* arm only (`expect.rev` mismatch): the row is still open — re-get + re-validate, zero backoff (a
|
|
1518
|
+
* cleanly KNOWN state is never "unknown"). */
|
|
1519
|
+
export type TerminalClaimOutcome = {
|
|
1520
|
+
claimed: true;
|
|
1521
|
+
} | {
|
|
1522
|
+
claimed: false;
|
|
1523
|
+
current: {
|
|
1524
|
+
status: "pending" | "resolved" | "expired";
|
|
1525
|
+
/** `(cp.rev ?? 0)` — the absent-rev-reads-as-0 rule the OCC key already uses. */
|
|
1526
|
+
rev: number;
|
|
1527
|
+
/** Row-field PASSTHROUGH on every present status: the row PRESERVES it across `reopen`
|
|
1528
|
+
* (resolved→pending rows carry it), `expire` never clears it, and a winnerless resolve
|
|
1529
|
+
* (resource/wake/task_done) does not overwrite an older value — so even
|
|
1530
|
+
* `(status:"resolved", outcome present)` is only "the row is resolved AND this is its
|
|
1531
|
+
* LAST RECORDED decision winner", never a store-level guarantee that THIS resolve minted
|
|
1532
|
+
* it. Interpreting the pair as a current decision is the CALLER's discipline (the
|
|
1533
|
+
* runner's gate-kind matching; a single-decision row schema like a deployment's ask
|
|
1534
|
+
* rows). See {@link CheckpointStore.claimTerminal} law 6. */
|
|
1535
|
+
resolvedOutcome?: ResolvedOutcome;
|
|
1536
|
+
}
|
|
1537
|
+
/** Row missing — OR wrong scope: the CAS predicate is the shared REF-A4 row match
|
|
1538
|
+
* ({@link checkpointRowMatches}), and a wrong-scope claim must lose WITHOUT reporting the
|
|
1539
|
+
* row's truth (multi-tenant isolation: cross-scope truth in a loss answer would be a read
|
|
1540
|
+
* bypass of the scope WHERE). Absent is a KNOWN state (zero backoff): expire intent ⇒ moot;
|
|
1541
|
+
* resolve intent ⇒ the caller's typed not-found path, no retry. */
|
|
1542
|
+
| {
|
|
1543
|
+
status: "absent";
|
|
1544
|
+
};
|
|
1545
|
+
};
|
|
1486
1546
|
/** A persisted suspension point: enough to resume a task on any replica. `status` drives the 3-state
|
|
1487
1547
|
* machine (pending → resolved | expired) that makes resume idempotent (§5). */
|
|
1488
1548
|
export interface Checkpoint {
|
|
@@ -2447,6 +2507,78 @@ export interface CheckpointStore {
|
|
|
2447
2507
|
* `resolve`/`reap`.
|
|
2448
2508
|
*/
|
|
2449
2509
|
expire(token: CheckpointToken, scope: string): Promise<boolean>;
|
|
2510
|
+
/**
|
|
2511
|
+
* design/384 slice 2 / S-25-R1 — the ATOMIC terminal claim: "win the terminal race" and "read the
|
|
2512
|
+
* truth on a loss" as ONE store operation. OPTIONAL, probed by presence (the `listByScope`
|
|
2513
|
+
* precedent); the runner's resume chain adopts it when present (the loser triages off `current`
|
|
2514
|
+
* with zero second read) and keeps the two-step resolve+get path otherwise. This JSDoc is the
|
|
2515
|
+
* single normative statement of the claim law — `put`'s create-once/read-back three-state and the
|
|
2516
|
+
* suspend saga's commit discipline are the same single-winner law's PUT variant and cross-reference
|
|
2517
|
+
* here rather than restate.
|
|
2518
|
+
*
|
|
2519
|
+
* The laws:
|
|
2520
|
+
* 1. **Atomicity** — the win/lose decision and the loser's `current` reading MUST come from the
|
|
2521
|
+
* same atomic unit (one `UPDATE … RETURNING` / one transaction / one single-threaded map
|
|
2522
|
+
* operation). A settle is self-sufficient: a caller holding `claimed:false` NEVER needs a
|
|
2523
|
+
* second read to act.
|
|
2524
|
+
* 2. **CAS-predicate identity** — `kind:"resolve"`'s win IS {@link resolve}'s win (same WHERE:
|
|
2525
|
+
* token+scope+pending[+rev]; a win records the winner via {@link winnerFromOutcome} and bumps
|
|
2526
|
+
* `rev` identically); `kind:"expire"` IS {@link expire}. ONE-MACHINE OBLIGATION: a store that
|
|
2527
|
+
* provides `claimTerminal` must route all three verbs through one protected write path
|
|
2528
|
+
* (resolve/expire implemented over claimTerminal, or all three over one shared CAS kernel) —
|
|
2529
|
+
* two predicates would be a drift window, and this law forbids it.
|
|
2530
|
+
* 3. **Timeout posture** — no timeout parameter here: transport deadlines belong to the backend,
|
|
2531
|
+
* wall-clock backstops to the caller. The contract states exactly two halves: a SETTLE carries
|
|
2532
|
+
* the truth; a REJECT proves NOTHING (the row may already be terminal — the caller must not
|
|
2533
|
+
* proclaim a terminal state from its own intent; fail-closed projection of "still pending"
|
|
2534
|
+
* stays legal). A LATE settle is as self-sufficient as a prompt one — an answer arriving after
|
|
2535
|
+
* the caller's own deadline still carries the true decision, so the caller settles on truth
|
|
2536
|
+
* with zero follow-up reads.
|
|
2537
|
+
* 4. **Recovery discipline — truth is comparable, ownership is not.** A bounded retry after a
|
|
2538
|
+
* reject re-sends the SAME claim (the CAS is idempotent). When the retry answers
|
|
2539
|
+
* `claimed:false`, `current` tells the caller what the row IS — never WHOSE claim made it so:
|
|
2540
|
+
* two claimants writing byte-identical outcomes cannot be told apart by content, and
|
|
2541
|
+
* {@link ResolvedOutcome} carries no claimant identity. Two consumption classes follow:
|
|
2542
|
+
* IDEMPOTENT consumption (settle on the recorded truth — who wrote it is irrelevant) may use
|
|
2543
|
+
* `claimed:false` + the recorded decision directly; RESPONSIBILITY-bearing consumption (only
|
|
2544
|
+
* the winner may execute the pending action — the runner's resume) must NEVER self-attribute
|
|
2545
|
+
* after ambiguity: treat it as a loss (the `already_resolved` shape, nothing executed) — the
|
|
2546
|
+
* same law as the existing crash-after-CAS pin ("retry sees resolved and does NOT re-execute"),
|
|
2547
|
+
* which this claim does not weaken. There is NO replay arm: a burned approval takes the
|
|
2548
|
+
* existing reopen/re-ask compensation route, and this contract opens no "assume approved and
|
|
2549
|
+
* replay" door. (A claimant-identity field would be a schema change; not here.) Expire-class
|
|
2550
|
+
* claims are naturally idempotent (expire-by-me ≡ expire-by-reaper; no attribution needed).
|
|
2551
|
+
* 5. **A loss reading is a consistent snapshot at the claim's linearization point, not an eternal
|
|
2552
|
+
* truth** — `resolved` can be reopened back to pending, so "settle is self-sufficient" scopes
|
|
2553
|
+
* to the disposition of THIS loss (settle on truth / re-validate on OCC / close on absent),
|
|
2554
|
+
* never to the row's future; a caller needing current state later still `get`s. `current` is a
|
|
2555
|
+
* projection, not the row (no state / pendingAction payloads).
|
|
2556
|
+
* 6. **`resolvedOutcome` reads as the row field it is, passed through verbatim** — the persisted
|
|
2557
|
+
* winner record: written only by decision-bearing resolves ({@link winnerFromOutcome} answers
|
|
2558
|
+
* `undefined` for resource/wake/task_done), PRESERVED by `reopen`, not cleared by later
|
|
2559
|
+
* winnerless resolves. In a loss answer it means "the row's last recorded decision winner",
|
|
2560
|
+
* NOT "the payload of the claim that beat you"; absence means "no decision winner recorded",
|
|
2561
|
+
* not "the winner had no content". For has-a-human-decided consumption this is exactly the
|
|
2562
|
+
* needed semantics; "who beat me / with what" is claimant-identity territory this contract
|
|
2563
|
+
* does not mint.
|
|
2564
|
+
* 7. **No declaration bit** — presence of the method IS the contract (an implementation
|
|
2565
|
+
* obligation at the same level as `resolve`'s CAS promise); no `redecision`-style declaration,
|
|
2566
|
+
* because that family guards against a stub satisfying a type, and a `claimTerminal` stub has
|
|
2567
|
+
* no such shape — the return form itself carries the obligation.
|
|
2568
|
+
*
|
|
2569
|
+
* Deployment correspondence (an ask-row store implementing the same law maps its own vocabulary):
|
|
2570
|
+
* token+scope ↔ the ask id (+ its run binding); pending/resolved/expired ↔ the ask row's
|
|
2571
|
+
* open/decided/terminal states; intent `resolve(outcome)` ↔ the human-decision verb; intent
|
|
2572
|
+
* `expire` ↔ the expire/cancel verbs; the `claimed:false → (status, resolvedOutcome)` PAIRED read
|
|
2573
|
+
* ↔ the decided row's recorded decision (reading the pair as the CURRENT decision is the CALLER's
|
|
2574
|
+
* discipline here — gate-kind matching — while a single-decision row schema satisfies it
|
|
2575
|
+
* structurally). Two shape notes carried from that correspondence: a store whose rows have NO
|
|
2576
|
+
* OCC/rev axis simply never produces the `status:"pending"` loss arm (an honestly absent arm is
|
|
2577
|
+
* correct — do not fabricate a rev); a store whose domain has NO reopen verb enjoys stronger
|
|
2578
|
+
* monotonicity than law 5 assumes — consumption written against law 5's weaker snapshot reading
|
|
2579
|
+
* stays correct there unchanged.
|
|
2580
|
+
*/
|
|
2581
|
+
claimTerminal?(token: CheckpointToken, scope: string, intent: TerminalClaimIntent): Promise<TerminalClaimOutcome>;
|
|
2450
2582
|
/**
|
|
2451
2583
|
* CAS-expire `pending` checkpoints in `scope` whose `deadline` has passed (`deadline <= cutoff`):
|
|
2452
2584
|
* `pending → expired`. Returns the count expired (for metrics). Idempotent across replicas (DB
|
|
@@ -2589,7 +2721,14 @@ export type CheckpointFaultMode =
|
|
|
2589
2721
|
"resolve-after-commit"
|
|
2590
2722
|
/** `resolve` throws *before* the CAS — simulates a crash before the commit; the row stays `pending`
|
|
2591
2723
|
* so a retry can still win it. */
|
|
2592
|
-
| "resolve-before-commit"
|
|
2724
|
+
| "resolve-before-commit"
|
|
2725
|
+
/** design/384 slice 2 — `claimTerminal` wins its claim (the commit lands) then throws before the
|
|
2726
|
+
* caller is acked: the claim-law recovery scenario (law 3/4 — a reject proves nothing; the retry's
|
|
2727
|
+
* loss answer carries the truth and a responsibility-bearing consumer must NOT self-attribute). */
|
|
2728
|
+
| "claim-after-commit"
|
|
2729
|
+
/** design/384 slice 2 — `claimTerminal` throws *before* its CAS: the row is untouched (still
|
|
2730
|
+
* pending), so a retry can still win the same claim. */
|
|
2731
|
+
| "claim-before-commit";
|
|
2593
2732
|
/**
|
|
2594
2733
|
* Default in-process {@link CheckpointStore}. Single-instance / tests only — it does NOT survive a
|
|
2595
2734
|
* restart or span replicas, so it cannot deliver the cross-process guarantee a durable backend does.
|
|
@@ -2613,7 +2752,54 @@ export declare class InMemoryCheckpointStore implements CheckpointStore {
|
|
|
2613
2752
|
private fault;
|
|
2614
2753
|
put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
|
|
2615
2754
|
get(token: CheckpointToken): Promise<Checkpoint | null>;
|
|
2755
|
+
/** The loser's single-round-trip reading (claim law 1): what the row IS, in its own vocabulary —
|
|
2756
|
+
* or `absent` for a missing row AND a wrong-scope row alike (the isolation rule: cross-scope truth
|
|
2757
|
+
* in a loss answer would be a read bypass of the scope WHERE). Cloned projection, never a live ref. */
|
|
2758
|
+
protected claimLossFrom(cp: Checkpoint | undefined, scope: string): TerminalClaimOutcome;
|
|
2759
|
+
/**
|
|
2760
|
+
* The ONE protected write core for the terminal-resolve claim — `resolve` and
|
|
2761
|
+
* `claimTerminal({kind:"resolve"})` both run exactly this (claim law 2's one-machine obligation:
|
|
2762
|
+
* one predicate, one commit, no drift window). Single-threaded JS makes the check + flip atomic; a
|
|
2763
|
+
* durable backend folds the same predicate into its CAS WHERE clause.
|
|
2764
|
+
*
|
|
2765
|
+
* ORDER (design/384 slice 2, healing the pre-existing resolve shape): the winner is derived and
|
|
2766
|
+
* CLONED — the point where an uncloneable `updatedInput` says so — BEFORE any field is written, and
|
|
2767
|
+
* the commit is then one uninterrupted write group (status+rev+winner+reopenReason). The old order
|
|
2768
|
+
* cloned AFTER the status/rev flip, so an uncloneable winner threw with the row already resolved
|
|
2769
|
+
* and NO winner recorded; now the same throw leaves the row byte-identical (still pending) — the
|
|
2770
|
+
* claim REJECTS (a reject proves nothing, law 3) instead of half-committing.
|
|
2771
|
+
*/
|
|
2772
|
+
protected claimResolveCore(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): TerminalClaimOutcome;
|
|
2773
|
+
/** The one protected write core for the terminal-expire claim — `expire` and
|
|
2774
|
+
* `claimTerminal({kind:"expire"})` both run exactly this (claim law 2, same as the resolve core). */
|
|
2775
|
+
protected claimExpireCore(token: CheckpointToken, scope: string): TerminalClaimOutcome;
|
|
2616
2776
|
resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>;
|
|
2777
|
+
/** design/384 slice 2 — the atomic terminal claim (the interface JSDoc is the law's single
|
|
2778
|
+
* normative statement). Implemented OVER the same protected cores `resolve`/`expire` run, which is
|
|
2779
|
+
* the one-machine obligation discharged rather than restated. ONE fault plane with the one
|
|
2780
|
+
* machine: a fault armed under the `resolve-*` names fires on the claim route of the SAME CAS too
|
|
2781
|
+
* (`kind:"resolve"` only — expire never consumed resolve faults), so a crash simulation pinned
|
|
2782
|
+
* against the two-step verb keeps firing byte-identically when the runner adopts the claim form.
|
|
2783
|
+
*
|
|
2784
|
+
* INHERITANCE OBLIGATION (a subclass provides `claimTerminal` whether it means to or not): a
|
|
2785
|
+
* subclass that overrides `resolve`/`expire` to wrap the CAS (audit hooks, fault injection,
|
|
2786
|
+
* interleave fixtures) must override THIS method consistently. Overriding the `protected` cores
|
|
2787
|
+
* (`claimResolveCore`/`claimExpireCore`) instead covers every COMMIT on every route — the cores
|
|
2788
|
+
* are the one write core all three verbs share — but NOT every ATTEMPT: `resolve`/`expire`
|
|
2789
|
+
* evaluate the shared row predicate themselves and return `false` without entering a core when
|
|
2790
|
+
* the row is not pending (missing, another scope, already terminal), while this claim route
|
|
2791
|
+
* enters the core unconditionally and takes its loss reading from it. A core override therefore
|
|
2792
|
+
* sees every winning claim and every OCC loss, but a row-predicate loss only when it arrived via
|
|
2793
|
+
* the claim route; a hook that must record EVERY attempt, including those losses, on EVERY route
|
|
2794
|
+
* wraps the three public verbs consistently. The same applies to a DELEGATING WRAPPER (Proxy / hand-built object)
|
|
2795
|
+
* that forwards `claimTerminal` while hooking only `resolve`: forwarding IS providing, and the
|
|
2796
|
+
* claim route is then the one that actually runs. A resolve-only wrap goes dark the moment a
|
|
2797
|
+
* consumer prefers the claim route (the runner's resume does), which is a one-machine-law
|
|
2798
|
+
* violation the SUBCLASS created — the base class cannot route through the public verbs
|
|
2799
|
+
* instead, because an `await` between the CAS and the loss reading would break the law-1
|
|
2800
|
+
* atomic unit this method exists to provide. (A wrapper that OMITS the member opts out
|
|
2801
|
+
* cleanly: the consumer's presence probe then keeps the two-step path, hooks intact.) */
|
|
2802
|
+
claimTerminal(token: CheckpointToken, scope: string, intent: TerminalClaimIntent): Promise<TerminalClaimOutcome>;
|
|
2617
2803
|
reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
|
|
2618
2804
|
setPendingSteer(token: CheckpointToken, scope: string, steer: PendingSteerInput): Promise<boolean>;
|
|
2619
2805
|
expire(token: CheckpointToken, scope: string): Promise<boolean>;
|
|
@@ -527,29 +527,71 @@ export class InMemoryCheckpointStore {
|
|
|
527
527
|
const cp = this.cps.get(token);
|
|
528
528
|
return cp ? structuredClone(cp) : null;
|
|
529
529
|
}
|
|
530
|
-
|
|
531
|
-
if (
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
530
|
+
claimLossFrom(cp, scope) {
|
|
531
|
+
if (cp === undefined || cp.scope !== scope)
|
|
532
|
+
return { claimed: false, current: { status: "absent" } };
|
|
533
|
+
return {
|
|
534
|
+
claimed: false,
|
|
535
|
+
current: {
|
|
536
|
+
status: cp.status,
|
|
537
|
+
rev: cp.rev ?? 0,
|
|
538
|
+
...(cp.resolvedOutcome !== undefined ? { resolvedOutcome: structuredClone(cp.resolvedOutcome) } : {}),
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
claimResolveCore(token, scope, outcome, expect) {
|
|
535
543
|
const cp = this.cps.get(token);
|
|
536
544
|
if (!checkpointRowMatches(cp, scope, "pending")) {
|
|
537
|
-
return
|
|
545
|
+
return this.claimLossFrom(cp, scope);
|
|
538
546
|
}
|
|
539
547
|
if (!checkpointOccMatches(cp, expect)) {
|
|
540
|
-
return
|
|
548
|
+
return this.claimLossFrom(cp, scope);
|
|
541
549
|
}
|
|
550
|
+
const winner = winnerFromOutcome(outcome);
|
|
551
|
+
const winnerClone = winner ? structuredClone(winner) : undefined;
|
|
542
552
|
cp.status = "resolved";
|
|
543
553
|
cp.rev = (cp.rev ?? 0) + 1;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
cp.resolvedOutcome = structuredClone(winner);
|
|
554
|
+
if (winnerClone)
|
|
555
|
+
cp.resolvedOutcome = winnerClone;
|
|
547
556
|
cp.reopenReason = undefined;
|
|
548
|
-
|
|
557
|
+
return { claimed: true };
|
|
558
|
+
}
|
|
559
|
+
claimExpireCore(token, scope) {
|
|
560
|
+
const cp = this.cps.get(token);
|
|
561
|
+
if (!checkpointRowMatches(cp, scope, "pending")) {
|
|
562
|
+
return this.claimLossFrom(cp, scope);
|
|
563
|
+
}
|
|
564
|
+
cp.status = "expired";
|
|
565
|
+
return { claimed: true };
|
|
566
|
+
}
|
|
567
|
+
async resolve(token, scope, outcome, expect) {
|
|
568
|
+
if (this.fault === "resolve-before-commit") {
|
|
569
|
+
this.fault = null;
|
|
570
|
+
throw new Error("injected fault: resolve before commit");
|
|
571
|
+
}
|
|
572
|
+
if (!checkpointRowMatches(this.cps.get(token), scope, "pending")) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
const out = this.claimResolveCore(token, scope, outcome, expect);
|
|
576
|
+
if (out.claimed && this.fault === "resolve-after-commit") {
|
|
549
577
|
this.fault = null;
|
|
550
578
|
throw new Error("injected fault: resolve after commit");
|
|
551
579
|
}
|
|
552
|
-
return
|
|
580
|
+
return out.claimed;
|
|
581
|
+
}
|
|
582
|
+
async claimTerminal(token, scope, intent) {
|
|
583
|
+
if (this.fault === "claim-before-commit" || (intent.kind === "resolve" && this.fault === "resolve-before-commit")) {
|
|
584
|
+
const name = this.fault === "claim-before-commit" ? "claim" : "resolve";
|
|
585
|
+
this.fault = null;
|
|
586
|
+
throw new Error(`injected fault: ${name} before commit`);
|
|
587
|
+
}
|
|
588
|
+
const out = intent.kind === "resolve" ? this.claimResolveCore(token, scope, intent.outcome, intent.expect) : this.claimExpireCore(token, scope);
|
|
589
|
+
if (out.claimed && (this.fault === "claim-after-commit" || (intent.kind === "resolve" && this.fault === "resolve-after-commit"))) {
|
|
590
|
+
const name = this.fault === "claim-after-commit" ? "claim" : "resolve";
|
|
591
|
+
this.fault = null;
|
|
592
|
+
throw new Error(`injected fault: ${name} after commit`);
|
|
593
|
+
}
|
|
594
|
+
return out;
|
|
553
595
|
}
|
|
554
596
|
async reopen(token, scope, reason) {
|
|
555
597
|
const cp = this.cps.get(token);
|
|
@@ -571,12 +613,10 @@ export class InMemoryCheckpointStore {
|
|
|
571
613
|
return true;
|
|
572
614
|
}
|
|
573
615
|
async expire(token, scope) {
|
|
574
|
-
|
|
575
|
-
if (!checkpointRowMatches(cp, scope, "pending")) {
|
|
616
|
+
if (!checkpointRowMatches(this.cps.get(token), scope, "pending")) {
|
|
576
617
|
return false;
|
|
577
618
|
}
|
|
578
|
-
|
|
579
|
-
return true;
|
|
619
|
+
return this.claimExpireCore(token, scope).claimed;
|
|
580
620
|
}
|
|
581
621
|
async reap(scope, cutoff) {
|
|
582
622
|
let n = 0;
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -1220,13 +1220,14 @@ export interface ToolGateInput {
|
|
|
1220
1220
|
/**
|
|
1221
1221
|
* design/384 slice 1 — the PER-CALL abort signal, carried in from the `tool_call` hook event's
|
|
1222
1222
|
* own `signal` seat (the loop's turn-scoped controller; the run abort is composed into it). The
|
|
1223
|
-
* gate itself never awaits on it — it hands the signal verbatim to the
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1229
|
-
*
|
|
1223
|
+
* gate itself never awaits on it — it hands the signal verbatim to the four consuming seats
|
|
1224
|
+
* below (`adjudicate`, `resolveAsk`, `resolveContentAsk`, and — slice 2 — `suspendAsk`, whose
|
|
1225
|
+
* park closure composes it into the halt×park fence) as their optional trailing argument, and
|
|
1226
|
+
* the Runner's closures compose it with the run signal so a turn interrupt (bare halt /
|
|
1227
|
+
* steer-now) releases the wait — or, on the durable leg, makes the park concede. Absent (a host
|
|
1228
|
+
* driving the gate directly, or an unthreaded emit) ⇒ every seat is invoked exactly as before —
|
|
1229
|
+
* same arity, no trailing `undefined` (the threading is a conditional invocation) — and waits
|
|
1230
|
+
* stay bound to the run signal alone: the historical shape, byte-identical.
|
|
1230
1231
|
*/
|
|
1231
1232
|
callSignal?: AbortSignal;
|
|
1232
1233
|
/** The combined tool-policy check (abort-bound), or undefined when no policy is wired (→ allow).
|
|
@@ -1314,7 +1315,13 @@ export interface ToolGateInput {
|
|
|
1314
1315
|
* rule the lane would then refuse to honor. APPENDED at the tail for the reason its neighbour
|
|
1315
1316
|
* states — every parameter here is positional — and typed `boolean` so a mis-bind against the two
|
|
1316
1317
|
* trailing `string` seats is a compile error rather than a silent re-binding. */
|
|
1317
|
-
probeMandated?: boolean
|
|
1318
|
+
probeMandated?: boolean,
|
|
1319
|
+
/** design/384 slice 2 — {@link ToolGateInput.callSignal}, threaded VERBATIM as a CONDITIONAL
|
|
1320
|
+
* trailing argument (the slice-1 arity discipline: absent means the historical arity, never a
|
|
1321
|
+
* trailing `undefined`). The park closure composes it with the run signal into the halt×park
|
|
1322
|
+
* fence's cut signal; an implementation that ignores it keeps the run-level binding. APPENDED at
|
|
1323
|
+
* the tail like its neighbours, and typed `AbortSignal` so a mis-bind is a compile error. */
|
|
1324
|
+
callSignal?: AbortSignal) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
|
|
1318
1325
|
/**
|
|
1319
1326
|
* design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
|
|
1320
1327
|
* before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
|
package/dist/core/hooks.js
CHANGED
|
@@ -859,7 +859,8 @@ export async function runToolGate(input) {
|
|
|
859
859
|
? { origin: orgAskOrigin !== undefined ? `org_${orgAskOrigin}` : "policy" }
|
|
860
860
|
: undefined;
|
|
861
861
|
if (suspendAsk && decision.action === "ask") {
|
|
862
|
-
const
|
|
862
|
+
const parkArgs = [req, currentInput, safety, undefined, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined, decision.action === "ask" ? decision.probeMandated : undefined];
|
|
863
|
+
const suspended = await (callSignal !== undefined ? suspendAsk(...parkArgs, callSignal) : suspendAsk(...parkArgs));
|
|
863
864
|
if (suspended) {
|
|
864
865
|
if ("parkFailed" in suspended)
|
|
865
866
|
parkFailed = suspended.parkFailed;
|
|
@@ -878,7 +879,8 @@ export async function runToolGate(input) {
|
|
|
878
879
|
req.args = outcome.presentedInput;
|
|
879
880
|
}
|
|
880
881
|
if (suspendAsk && outcome.parkDeclined && parkFailed === undefined) {
|
|
881
|
-
const
|
|
882
|
+
const parkArgs = [req, currentInput, safety, true, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined, decision.action === "ask" ? decision.probeMandated : undefined];
|
|
883
|
+
const suspended = await (callSignal !== undefined ? suspendAsk(...parkArgs, callSignal) : suspendAsk(...parkArgs));
|
|
882
884
|
if (suspended) {
|
|
883
885
|
if ("parkFailed" in suspended)
|
|
884
886
|
parkFailed = suspended.parkFailed;
|
|
@@ -910,7 +912,8 @@ export async function runToolGate(input) {
|
|
|
910
912
|
askDenyResolution = resolved.resolution;
|
|
911
913
|
decision = resolved;
|
|
912
914
|
if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
|
|
913
|
-
const
|
|
915
|
+
const parkArgs = [req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.segmentCoverage : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.matchedAskRule : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeMandated : undefined];
|
|
916
|
+
const suspended = await (callSignal !== undefined ? suspendAsk(...parkArgs, callSignal) : suspendAsk(...parkArgs));
|
|
914
917
|
if (suspended) {
|
|
915
918
|
if ("parkFailed" in suspended)
|
|
916
919
|
parkFailed = suspended.parkFailed;
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* this by editing a file backend's file. That is the settings-file trust model, stated rather than
|
|
28
28
|
* defended against: for a file backend, host = user, no more and no less.
|
|
29
29
|
*/
|
|
30
|
-
import { type RuleOffer, type RuleRejectCode, type RuleScope, type RuleDot } from "./permission-rule-model.js";
|
|
30
|
+
import { type EditedRuleBreadthWarning, type UncoveredSegmentDetail, type RuleOffer, type RuleRejectCode, type RuleScope, type RuleDot } from "./permission-rule-model.js";
|
|
31
31
|
import type { PermissionRuleStoreProvider, RuleOwner } from "./permission-rule-store.js";
|
|
32
32
|
import type { SessionRuleOverlay } from "./permission-rule-session.js";
|
|
33
33
|
/** One candidate rule inside an approval record: the exact text and where it would apply. */
|
|
@@ -48,10 +48,16 @@ export type RuleApprovalKind = "card" | "import" | "starter";
|
|
|
48
48
|
* · `kind: "batch"` — a CONJUNCTION batch: confirming it is one yes to ALL of `candidates` at
|
|
49
49
|
* once, with no per-member sub-selection. On a CARD record the batch carries two MINT-TIME
|
|
50
50
|
* facts that cannot be re-derived from candidate texts and therefore must be stored:
|
|
51
|
-
* `segments` (each member's source segment, index-aligned with `candidates`)
|
|
51
|
+
* `segments` (each member's source segment, index-aligned with `candidates`),
|
|
52
52
|
* `uncoveredSegments` (the honest surplus count the wire offer disclosed, judged on the
|
|
53
|
-
* prepare-time coverage snapshot)
|
|
54
|
-
*
|
|
53
|
+
* prepare-time coverage snapshot) and — since `schema: 3` (design/382 §3.5) — `uncoveredDetail`,
|
|
54
|
+
* one reason row per counted segment in segment order: WHY each stayed uncovered is a mint-time
|
|
55
|
+
* fact that cannot be re-derived from candidate texts, so it must be stored or a crash/reload
|
|
56
|
+
* rebuild would render a card that says less than the first render did (record-is-what-you-see).
|
|
57
|
+
* REQUIRED on a card batch, all three: "optional, validated when present" is the
|
|
58
|
+
* silent-absence-passes shape this record form already refused once, and it is not re-imported
|
|
59
|
+
* for the new seat. On an import/starter record there is no compound command
|
|
60
|
+
* and no coverage snapshot, so all three fields must be ABSENT — honest absence, never an
|
|
55
61
|
* empty-string stand-in. The read-side integrity checks refuse either direction's violation.
|
|
56
62
|
*
|
|
57
63
|
* Cardinality: a batch references 1..N candidates where N is the record's own candidate count.
|
|
@@ -67,6 +73,7 @@ export type RuleOffer2 = {
|
|
|
67
73
|
readonly candidates: readonly number[];
|
|
68
74
|
readonly segments?: readonly string[];
|
|
69
75
|
readonly uncoveredSegments?: number;
|
|
76
|
+
readonly uncoveredDetail?: readonly UncoveredSegmentDetail[];
|
|
70
77
|
};
|
|
71
78
|
/** A durable record of one pending/settled human decision about minting rules.
|
|
72
79
|
*
|
|
@@ -81,12 +88,16 @@ export interface RuleApprovalRecord {
|
|
|
81
88
|
/** Structural owner (additive). Absent on every record a v1 tree minted. */
|
|
82
89
|
owner?: RuleOwner;
|
|
83
90
|
/**
|
|
84
|
-
* design/375 §4.5 — the record-form version stamp. A stored row WITHOUT it
|
|
85
|
-
* such a row confirms and redeems nothing (refused loudly,
|
|
86
|
-
* read through a compatibility shim: pending approvals are
|
|
87
|
-
* command draws a fresh card. Stores hand pre-form rows back
|
|
91
|
+
* design/375 §4.5 — the record-form version stamp. A stored row WITHOUT it — or carrying an older
|
|
92
|
+
* number — predates this form; such a row confirms and redeems nothing (refused loudly,
|
|
93
|
+
* `record_schema_stale`) and is never read through a compatibility shim: pending approvals are
|
|
94
|
+
* short-lived, and re-triggering the command draws a fresh card. Stores hand pre-form rows back
|
|
95
|
+
* as {@link StaleRuleApprovalRecord}. `3` (design/382 §3.5, B8/B7): the card batch's
|
|
96
|
+
* `uncoveredDetail` seat became REQUIRED and `edited.warnings` became storable — a `schema: 2`
|
|
97
|
+
* row is read back stale exactly like the pre-stamp rows before it (the short-lived-pending
|
|
98
|
+
* bargain, unchanged).
|
|
88
99
|
*/
|
|
89
|
-
schema:
|
|
100
|
+
schema: 3;
|
|
90
101
|
kind: RuleApprovalKind;
|
|
91
102
|
state: "pending" | "approved" | "redeemed";
|
|
92
103
|
candidates: RuleCandidate[];
|
|
@@ -137,18 +148,26 @@ export interface RuleApprovalRecord {
|
|
|
137
148
|
* same bytes); `at` is when the edit landed. Present ⇒ the record's chosen offer is the single
|
|
138
149
|
* offer minted over this candidate (the append, the offer and the choice are one CAS write).
|
|
139
150
|
* Absent on every card settled through an engine candidate.
|
|
151
|
+
*
|
|
152
|
+
* `warnings` (design/382 §3.3-5) — the BREADTH warning codes the text gates raised on this edit,
|
|
153
|
+
* written in the same CAS that settles it: "the width was named before the yes" becomes a fact the
|
|
154
|
+
* record itself carries, auditable after any crash or upgrade. Optional; present only when a
|
|
155
|
+
* warning was raised, and when present it is an array from the closed code set (the read-side
|
|
156
|
+
* integrity gate refuses anything else). Codes only — the sentences are derivable from the
|
|
157
|
+
* canonical rule text and are deliberately not stored (no second copy to drift).
|
|
140
158
|
*/
|
|
141
159
|
edited?: {
|
|
142
160
|
index: number;
|
|
143
161
|
text: string;
|
|
144
162
|
at: string;
|
|
163
|
+
warnings?: readonly EditedRuleBreadthWarning["code"][];
|
|
145
164
|
};
|
|
146
165
|
/** Dots already minted for this record, keyed by candidate index — the replay anchor. */
|
|
147
166
|
redeemedDots?: Record<number, RuleDot>;
|
|
148
167
|
}
|
|
149
168
|
/**
|
|
150
169
|
* design/375 §4.5 — the minimal ENVELOPE a store hands back for a durable row that predates the
|
|
151
|
-
* `schema:
|
|
170
|
+
* CURRENT (`schema: 3`) record form. A typed read cannot honestly return such a row as a
|
|
152
171
|
* {@link RuleApprovalRecord}, and dropping it at the store layer would misreport "stale" as
|
|
153
172
|
* corruption — so the store contract is this variant instead: `staleSchema: true` plus the
|
|
154
173
|
* IDENTITY fields, which are spelled the same way in every version and therefore safe to read.
|
|
@@ -209,6 +228,14 @@ export interface RuleConsentDeps {
|
|
|
209
228
|
* coverage reads see no session rows — both fail toward asking.
|
|
210
229
|
*/
|
|
211
230
|
sessionRules?: SessionRuleOverlay;
|
|
231
|
+
/**
|
|
232
|
+
* design/382 §2.4-6 — the deployment's sensitive-read negative control for the directoryRead MINT
|
|
233
|
+
* floor, same-source with its compiled read-deny judge (`(d) => readDeny.matchPath(d) !== null` is
|
|
234
|
+
* the canonical wiring). Consulted only when a card would PROPOSE a directory authorization from a
|
|
235
|
+
* cd segment — never by the explicit-consent doors (hand-written, import), which carry their own
|
|
236
|
+
* preview and validation. Absent ⇒ the engine's built-in default deny table stands in (refusal-only).
|
|
237
|
+
*/
|
|
238
|
+
deniesDirectoryRead?: (directory: string) => boolean;
|
|
212
239
|
}
|
|
213
240
|
/** In-memory approval records — the test backend and the reference CAS semantics, the stale-row
|
|
214
241
|
* envelope read included. */
|
|
@@ -297,6 +324,17 @@ export declare function prepareCardApproval(opts: {
|
|
|
297
324
|
* leans wide — the over-offer direction, which redemption's per-rule validation keeps safe.
|
|
298
325
|
*/
|
|
299
326
|
cwd?: string;
|
|
327
|
+
/**
|
|
328
|
+
* design/382 §2.4 (adversarial-review r2) — the EXECUTION working directory of the adjudicated
|
|
329
|
+
* call: the live tracked cwd the shell will resolve a relative `cd ./x` against, threaded by the
|
|
330
|
+
* caller exactly like `cwd` (the runner's ask-projection seat threads its `handsCwdRef.current`;
|
|
331
|
+
* a host preparing cards for its own calls threads whatever base those calls execute under).
|
|
332
|
+
* ONLY the relative-cd resolution reads it; scope eligibility and the derived project scope stay
|
|
333
|
+
* on `cwd`. Absent ⇒ `cwd` is the base (the single-value deployment, where the two coincide).
|
|
334
|
+
* Present and DIFFERENT, it is what keeps this durable consent path and the advisory ask-time
|
|
335
|
+
* projection minting the SAME directory member — two faces reading one world.
|
|
336
|
+
*/
|
|
337
|
+
execCwd?: string;
|
|
300
338
|
deps: RuleConsentDeps;
|
|
301
339
|
}): Promise<{
|
|
302
340
|
approvalId: string;
|
|
@@ -346,7 +384,7 @@ export declare function confirmRuleApproval(opts: {
|
|
|
346
384
|
/** Why a confirmation did not land. A closed set so a host can branch (re-present, re-fetch, give up).
|
|
347
385
|
* The two `record_*` members past `record_not_found` are the durable-row gates (design/375 §4.5/§4.7),
|
|
348
386
|
* disclosed to the record's own owner only — a non-owner gets `record_not_found` for every row:
|
|
349
|
-
* - `"record_schema_stale"` — the record predates the `schema:
|
|
387
|
+
* - `"record_schema_stale"` — the record predates the current (`schema: 3`) form; the decision it carried
|
|
350
388
|
* is void, and the recovery path is re-triggering the command for a fresh card (no
|
|
351
389
|
* compatibility read, no migration — `detail.message` says so);
|
|
352
390
|
* - `"record_malformed"` — the record's offer structure failed the read-side integrity checks
|
|
@@ -394,6 +432,16 @@ export type ConfirmResult = {
|
|
|
394
432
|
export type EditedRuleTextPrecheck = {
|
|
395
433
|
ok: true;
|
|
396
434
|
canonicalRule: string;
|
|
435
|
+
/**
|
|
436
|
+
* design/382 §3.3-5 — BREADTH notes about the accepted rule (additive; absent = nothing to
|
|
437
|
+
* say). A warning is never a refusal: the muscle-memory wide forms are legal (§3.4), and this
|
|
438
|
+
* seat is what makes accepting one an informed act. Rendering them beside the input line is
|
|
439
|
+
* the surface's duty (the engine cannot force a display); what the ENGINE guarantees is the
|
|
440
|
+
* durable half — a confirmation through the edit arm writes the hit codes into the record
|
|
441
|
+
* (`edited.warnings`), so "the width was named before the yes" is an auditable fact. The
|
|
442
|
+
* confirmation result deliberately does NOT repeat them (one fact, one loud place).
|
|
443
|
+
*/
|
|
444
|
+
warnings?: readonly EditedRuleBreadthWarning[];
|
|
397
445
|
} | {
|
|
398
446
|
ok: false;
|
|
399
447
|
code?: RuleRejectCode;
|
|
@@ -483,19 +531,20 @@ export interface ImportPreview {
|
|
|
483
531
|
reason: string;
|
|
484
532
|
}>;
|
|
485
533
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
* Exactly ONE row per candidate the translation produced, in candidate order, carrying that
|
|
490
|
-
* candidate's `scope`. The scope is load-bearing rather than decoration: the same file line can
|
|
491
|
-
* appear in two layers and become two candidates in two scopes, and a report keyed on the text alone
|
|
492
|
-
* would show one row for two consents (independent-review finding — the row count and the candidate
|
|
493
|
-
* count must be readable against each other).
|
|
534
|
+
* ALWAYS EMPTY in this version — a compatibility seat, kept on the wire shape so an existing reader
|
|
535
|
+
* keeps finding the key (removing a wire field would break readers for no gain).
|
|
494
536
|
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
537
|
+
* The seat once carried the entries whose MATCH FORM was rewritten on the way in (the space-star
|
|
538
|
+
* suggestion form of the other product translated to this lane's colon-star form, one row per
|
|
539
|
+
* translated candidate with its `scope`). That translation is retired: the space-star wildcard is
|
|
540
|
+
* now a first-class match form of this lane's own grammar, so an entry's match form imports as the
|
|
541
|
+
* file states it — `Bash(npm run *)` lands as `Bash(npm run *)`, never as a colon-star — and nothing
|
|
542
|
+
* is ever pushed here. What `candidates` carries is the grammar's CANONICAL re-projection of each
|
|
543
|
+
* entry (the parser folds spacing: `Bash(npm<TAB>run *)` lands as `Bash(npm run *)`); that is the
|
|
544
|
+
* ordinary normalization every rule text in this lane undergoes, the same one the store applies,
|
|
545
|
+
* and not a translation this seat reports. An entry whose star placement has no rule form is
|
|
546
|
+
* refused into `skipped` under `unsupported.wildcard`, named as the file writes it. A reader
|
|
547
|
+
* rendering a "rewritten spellings" column from this seat renders an empty column, correctly.
|
|
499
548
|
*/
|
|
500
549
|
translated: Array<{
|
|
501
550
|
from: string;
|