instar 1.3.992 → 1.3.993
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/core/RecurrenceActuator.d.ts +79 -0
- package/dist/core/RecurrenceActuator.d.ts.map +1 -0
- package/dist/core/RecurrenceActuator.js +116 -0
- package/dist/core/RecurrenceActuator.js.map +1 -0
- package/dist/core/recurrenceLoop.d.ts +66 -0
- package/dist/core/recurrenceLoop.d.ts.map +1 -0
- package/dist/core/recurrenceLoop.js +76 -0
- package/dist/core/recurrenceLoop.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.993.md +73 -0
- package/upgrades/side-effects/recurrence-actuator.md +130 -0
- package/upgrades/side-effects/recurrence-loop.md +112 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecurrenceActuator — turns a recurrence finding into tracked work, once.
|
|
3
|
+
*
|
|
4
|
+
* `RecurrenceReader` makes recurrence VISIBLE. Visibility is not the goal: the
|
|
5
|
+
* project's whole diagnosis is that instar notices constantly and closes almost
|
|
6
|
+
* nothing (filing-to-completion ≈ 30:1). A reader that produces a beautiful
|
|
7
|
+
* report which nobody acts on would be the 30:1 ratio with better typography.
|
|
8
|
+
*
|
|
9
|
+
* Operator directive, 2026-07-26 20:08Z: *"the synthesis itself must lead to
|
|
10
|
+
* ACTION and a fully closed loop"*, with as little user dependence as possible.
|
|
11
|
+
*
|
|
12
|
+
* So this proposes ONE thing, through a path that already exists: for a cluster
|
|
13
|
+
* that genuinely recurs and that nobody has ever turned into work, create a
|
|
14
|
+
* tracked action on the EXISTING evolution action queue. That closes the loop —
|
|
15
|
+
* noticed repeatedly → becomes real work → appears where work is tracked → gets
|
|
16
|
+
* done or explicitly cancelled.
|
|
17
|
+
*
|
|
18
|
+
* WHAT THIS IS NOT:
|
|
19
|
+
* - Not a new notification channel. The single funniest way to fail at fixing
|
|
20
|
+
* "we notice things and never close them" is to build a fourth place that
|
|
21
|
+
* notices things. Nothing here notifies anybody.
|
|
22
|
+
* - Not authority. Creating a tracked action QUEUES work for a human or agent to
|
|
23
|
+
* judge. It does not close, prioritise, escalate, or act on anything.
|
|
24
|
+
* - Not a bulk importer. See "the fix must not become its own pile" below.
|
|
25
|
+
*/
|
|
26
|
+
import type { RecurrenceReport } from './RecurrenceReader.js';
|
|
27
|
+
/** A proposed piece of tracked work. The caller performs the actual write. */
|
|
28
|
+
export interface ProposedAction {
|
|
29
|
+
title: string;
|
|
30
|
+
description: string;
|
|
31
|
+
priority: 'critical' | 'high' | 'medium' | 'low';
|
|
32
|
+
/**
|
|
33
|
+
* Stable key derived from the cluster, so a re-run UPDATES rather than
|
|
34
|
+
* duplicates. Without this the actuator would itself become a generator of
|
|
35
|
+
* repeated noticings — the precise disease it treats.
|
|
36
|
+
*/
|
|
37
|
+
externalKey: string;
|
|
38
|
+
/** The cluster this came from, for the caller's audit trail. */
|
|
39
|
+
sourceKey: string;
|
|
40
|
+
observedCount: number;
|
|
41
|
+
}
|
|
42
|
+
export type ActuationRefusal = {
|
|
43
|
+
reason: 'actions-store-unreadable';
|
|
44
|
+
detail: string;
|
|
45
|
+
} | {
|
|
46
|
+
reason: 'no-qualifying-clusters';
|
|
47
|
+
detail: string;
|
|
48
|
+
};
|
|
49
|
+
export interface ActuationPlan {
|
|
50
|
+
/** Empty when `refused` is set. */
|
|
51
|
+
propose: ProposedAction[];
|
|
52
|
+
/** Set when the actuator declined to act at all, with why. */
|
|
53
|
+
refused?: ActuationRefusal;
|
|
54
|
+
/** Clusters that qualified but were held back by the per-run cap. */
|
|
55
|
+
deferredByCap: number;
|
|
56
|
+
/** Always present, so a caller can report honestly even on a refusal. */
|
|
57
|
+
consideredClusters: number;
|
|
58
|
+
}
|
|
59
|
+
export interface ActuationOptions {
|
|
60
|
+
/**
|
|
61
|
+
* A thing seen twice is not yet a pattern worth spending a work item on.
|
|
62
|
+
* Default 10: high enough that a proposal is obviously justified by volume.
|
|
63
|
+
*/
|
|
64
|
+
minCount?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Hard cap per run. 69 qualifying clusters turned into 69 action items would
|
|
67
|
+
* be a new backlog wearing a different hat — the fix becoming its own pile.
|
|
68
|
+
* A small cap converges across sessions instead, densest-first.
|
|
69
|
+
*/
|
|
70
|
+
maxPerRun?: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Decide what work to propose from a recurrence report.
|
|
74
|
+
*
|
|
75
|
+
* Pure: it returns a PLAN. The caller writes it, so the write path (and its
|
|
76
|
+
* gating) stays exactly where it already is.
|
|
77
|
+
*/
|
|
78
|
+
export declare function planActuation(report: RecurrenceReport, opts?: ActuationOptions): ActuationPlan;
|
|
79
|
+
//# sourceMappingURL=RecurrenceActuator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RecurrenceActuator.d.ts","sourceRoot":"","sources":["../../src/core/RecurrenceActuator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAqB,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEjF,8EAA8E;AAC9E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACjD;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,MAAM,EAAE,0BAA0B,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,MAAM,EAAE,wBAAwB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,MAAM,WAAW,aAAa;IAC5B,mCAAmC;IACnC,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,8DAA8D;IAC9D,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,qEAAqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,gBAAgB,EACxB,IAAI,GAAE,gBAAqB,GAC1B,aAAa,CAyDf"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecurrenceActuator — turns a recurrence finding into tracked work, once.
|
|
3
|
+
*
|
|
4
|
+
* `RecurrenceReader` makes recurrence VISIBLE. Visibility is not the goal: the
|
|
5
|
+
* project's whole diagnosis is that instar notices constantly and closes almost
|
|
6
|
+
* nothing (filing-to-completion ≈ 30:1). A reader that produces a beautiful
|
|
7
|
+
* report which nobody acts on would be the 30:1 ratio with better typography.
|
|
8
|
+
*
|
|
9
|
+
* Operator directive, 2026-07-26 20:08Z: *"the synthesis itself must lead to
|
|
10
|
+
* ACTION and a fully closed loop"*, with as little user dependence as possible.
|
|
11
|
+
*
|
|
12
|
+
* So this proposes ONE thing, through a path that already exists: for a cluster
|
|
13
|
+
* that genuinely recurs and that nobody has ever turned into work, create a
|
|
14
|
+
* tracked action on the EXISTING evolution action queue. That closes the loop —
|
|
15
|
+
* noticed repeatedly → becomes real work → appears where work is tracked → gets
|
|
16
|
+
* done or explicitly cancelled.
|
|
17
|
+
*
|
|
18
|
+
* WHAT THIS IS NOT:
|
|
19
|
+
* - Not a new notification channel. The single funniest way to fail at fixing
|
|
20
|
+
* "we notice things and never close them" is to build a fourth place that
|
|
21
|
+
* notices things. Nothing here notifies anybody.
|
|
22
|
+
* - Not authority. Creating a tracked action QUEUES work for a human or agent to
|
|
23
|
+
* judge. It does not close, prioritise, escalate, or act on anything.
|
|
24
|
+
* - Not a bulk importer. See "the fix must not become its own pile" below.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Decide what work to propose from a recurrence report.
|
|
28
|
+
*
|
|
29
|
+
* Pure: it returns a PLAN. The caller writes it, so the write path (and its
|
|
30
|
+
* gating) stays exactly where it already is.
|
|
31
|
+
*/
|
|
32
|
+
export function planActuation(report, opts = {}) {
|
|
33
|
+
const minCount = opts.minCount ?? 10;
|
|
34
|
+
const maxPerRun = opts.maxPerRun ?? 3;
|
|
35
|
+
// THE REFUSAL, and it is sharper than the reader's.
|
|
36
|
+
//
|
|
37
|
+
// The reader withholds a VERDICT on any partial read. The actuator must
|
|
38
|
+
// withhold the ACTION — but only one missing store actually invalidates the
|
|
39
|
+
// decision, and conflating them would be lazy symmetry:
|
|
40
|
+
//
|
|
41
|
+
// attention/sentinel unreadable → the reader saw FEWER observations. Counts
|
|
42
|
+
// are understated, so a cluster that qualifies still genuinely qualifies.
|
|
43
|
+
// Acting is conservative and safe.
|
|
44
|
+
//
|
|
45
|
+
// ACTIONS unreadable → `tracked` is UNKNOWABLE for every cluster, because
|
|
46
|
+
// `tracked` means "a member came from the action queue". Every cluster
|
|
47
|
+
// would look untracked. Acting would duplicate work that may already
|
|
48
|
+
// exist — the actuator would manufacture the exact redundancy it exists to
|
|
49
|
+
// remove, and do it under the banner of fixing it.
|
|
50
|
+
//
|
|
51
|
+
// So: actions-store unreadable ⇒ propose NOTHING, and say why.
|
|
52
|
+
const actionsUnreadable = report.coverage.unreadable.find((u) => u.store === 'actions');
|
|
53
|
+
if (actionsUnreadable) {
|
|
54
|
+
return {
|
|
55
|
+
propose: [],
|
|
56
|
+
refused: {
|
|
57
|
+
reason: 'actions-store-unreadable',
|
|
58
|
+
detail: `the action queue could not be read (${actionsUnreadable.reason}), so "has anyone already ` +
|
|
59
|
+
'committed to this?" is unanswerable for every cluster. Proposing work now would duplicate ' +
|
|
60
|
+
'whatever is already tracked. No actions proposed.',
|
|
61
|
+
},
|
|
62
|
+
deferredByCap: 0,
|
|
63
|
+
consideredClusters: report.clusters.length,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const qualifying = report.clusters.filter((c) => !c.tracked && c.count >= minCount);
|
|
67
|
+
if (qualifying.length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
propose: [],
|
|
70
|
+
refused: {
|
|
71
|
+
reason: 'no-qualifying-clusters',
|
|
72
|
+
detail: `no untracked cluster reached the minCount=${minCount} threshold`,
|
|
73
|
+
},
|
|
74
|
+
deferredByCap: 0,
|
|
75
|
+
consideredClusters: report.clusters.length,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const selected = qualifying.slice(0, maxPerRun);
|
|
79
|
+
return {
|
|
80
|
+
propose: selected.map(toProposedAction),
|
|
81
|
+
deferredByCap: qualifying.length - selected.length,
|
|
82
|
+
consideredClusters: report.clusters.length,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Priority from volume alone — deterministic, no model, no judgment. */
|
|
86
|
+
function priorityFor(count) {
|
|
87
|
+
if (count >= 100)
|
|
88
|
+
return 'high';
|
|
89
|
+
if (count >= 25)
|
|
90
|
+
return 'medium';
|
|
91
|
+
return 'low';
|
|
92
|
+
}
|
|
93
|
+
function toProposedAction(c) {
|
|
94
|
+
const span = c.firstSeen && c.lastSeen && c.firstSeen !== c.lastSeen
|
|
95
|
+
? ` between ${c.firstSeen} and ${c.lastSeen}`
|
|
96
|
+
: '';
|
|
97
|
+
return {
|
|
98
|
+
title: `Recurring, untracked: ${c.exemplar.slice(0, 90)}`,
|
|
99
|
+
description: `Noticed ${c.count} times${span} across ${c.stores.join(', ')}` +
|
|
100
|
+
(c.sources.length ? ` (sources: ${c.sources.slice(0, 5).join(', ')})` : '') +
|
|
101
|
+
`, and never turned into tracked work.\n\n` +
|
|
102
|
+
'Surfaced by RecurrenceReader, which groups observations across the attention queue, the ' +
|
|
103
|
+
'action queue and the sentinel log. The individual noticings are each minor and each true; ' +
|
|
104
|
+
'the VOLUME is the finding.\n\n' +
|
|
105
|
+
'This action exists so the recurrence is either fixed or deliberately dismissed, rather than ' +
|
|
106
|
+
'noticed a further N times. Cancelling it IS a valid outcome — an explicit decision beats ' +
|
|
107
|
+
'silent accumulation.',
|
|
108
|
+
priority: priorityFor(c.count),
|
|
109
|
+
// Stable across runs: the same cluster always maps to the same key, so a
|
|
110
|
+
// second pass updates one row instead of adding another.
|
|
111
|
+
externalKey: `recurrence:${c.key}`,
|
|
112
|
+
sourceKey: c.key,
|
|
113
|
+
observedCount: c.count,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=RecurrenceActuator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RecurrenceActuator.js","sourceRoot":"","sources":["../../src/core/RecurrenceActuator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recurrenceLoop — the caller that makes the loop actually close.
|
|
3
|
+
*
|
|
4
|
+
* `RecurrenceReader` sees. `RecurrenceActuator` decides. Neither touches a store,
|
|
5
|
+
* which is what keeps both pure and testable — but it also meant "the loop
|
|
6
|
+
* closes" was a DESIGNED property and not a demonstrated one. This is the piece
|
|
7
|
+
* that demonstrates it: it reads the three stores, plans, and writes the result
|
|
8
|
+
* through the caller-supplied action-creation function.
|
|
9
|
+
*
|
|
10
|
+
* It is deliberately the ONLY place in this feature that performs I/O, so every
|
|
11
|
+
* read failure has exactly one place to be reported from and cannot be swallowed
|
|
12
|
+
* somewhere in the middle.
|
|
13
|
+
*
|
|
14
|
+
* Operator directive 2026-07-26 20:08Z: synthesis must lead to ACTION, through
|
|
15
|
+
* paths that already exist, with the loop closed and minimal user dependence.
|
|
16
|
+
*/
|
|
17
|
+
import { type Observation, type RecurrenceReport } from './RecurrenceReader.js';
|
|
18
|
+
import { type ActuationOptions, type ProposedAction } from './RecurrenceActuator.js';
|
|
19
|
+
/** Reads one store. Returning a rejected promise is how it reports "unreadable". */
|
|
20
|
+
export type StoreReader = () => Promise<Observation[]>;
|
|
21
|
+
export interface LoopDeps {
|
|
22
|
+
readAttention: StoreReader;
|
|
23
|
+
readActions: StoreReader;
|
|
24
|
+
readSentinel: StoreReader;
|
|
25
|
+
/**
|
|
26
|
+
* Creates ONE tracked action. The caller supplies this so the write goes
|
|
27
|
+
* through whatever path it already uses, with whatever gating that path
|
|
28
|
+
* already has. This module never constructs an HTTP call itself.
|
|
29
|
+
*/
|
|
30
|
+
createAction: (a: ProposedAction) => Promise<{
|
|
31
|
+
id: string;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
export interface LoopResult {
|
|
35
|
+
report: RecurrenceReport;
|
|
36
|
+
/** Actions actually created, with the ids the store returned. */
|
|
37
|
+
created: {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
observedCount: number;
|
|
41
|
+
}[];
|
|
42
|
+
/** Set when the actuator declined; `created` is then empty. */
|
|
43
|
+
refused?: {
|
|
44
|
+
reason: string;
|
|
45
|
+
detail: string;
|
|
46
|
+
};
|
|
47
|
+
/** Qualifying clusters held back by the per-run cap. */
|
|
48
|
+
deferredByCap: number;
|
|
49
|
+
/**
|
|
50
|
+
* Proposals whose WRITE failed. Distinct from `refused` (a decision not to
|
|
51
|
+
* act) — this is a decision to act that did not land, and conflating the two
|
|
52
|
+
* would hide real breakage behind a principled-looking refusal.
|
|
53
|
+
*/
|
|
54
|
+
writeFailures: {
|
|
55
|
+
title: string;
|
|
56
|
+
error: string;
|
|
57
|
+
}[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Run one pass: read → group → plan → create.
|
|
61
|
+
*
|
|
62
|
+
* Never throws for an unreadable store — that is data, reported in
|
|
63
|
+
* `report.coverage`, and it is what the actuator's refusal reads.
|
|
64
|
+
*/
|
|
65
|
+
export declare function runRecurrenceLoop(deps: LoopDeps, opts?: ActuationOptions): Promise<LoopResult>;
|
|
66
|
+
//# sourceMappingURL=recurrenceLoop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recurrenceLoop.d.ts","sourceRoot":"","sources":["../../src/core/recurrenceLoop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAiB,KAAK,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAEpG,oFAAoF;AACpF,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;AAEvD,MAAM,WAAW,QAAQ;IACvB,aAAa,EAAE,WAAW,CAAC;IAC3B,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,EAAE,WAAW,CAAC;IAC1B;;;;OAIG;IACH,YAAY,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,gBAAgB,CAAC;IACzB,iEAAiE;IACjE,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAChE,+DAA+D;IAC/D,OAAO,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,wDAAwD;IACxD,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACnD;AAoBD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,QAAQ,EACd,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,UAAU,CAAC,CA0CrB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recurrenceLoop — the caller that makes the loop actually close.
|
|
3
|
+
*
|
|
4
|
+
* `RecurrenceReader` sees. `RecurrenceActuator` decides. Neither touches a store,
|
|
5
|
+
* which is what keeps both pure and testable — but it also meant "the loop
|
|
6
|
+
* closes" was a DESIGNED property and not a demonstrated one. This is the piece
|
|
7
|
+
* that demonstrates it: it reads the three stores, plans, and writes the result
|
|
8
|
+
* through the caller-supplied action-creation function.
|
|
9
|
+
*
|
|
10
|
+
* It is deliberately the ONLY place in this feature that performs I/O, so every
|
|
11
|
+
* read failure has exactly one place to be reported from and cannot be swallowed
|
|
12
|
+
* somewhere in the middle.
|
|
13
|
+
*
|
|
14
|
+
* Operator directive 2026-07-26 20:08Z: synthesis must lead to ACTION, through
|
|
15
|
+
* paths that already exist, with the loop closed and minimal user dependence.
|
|
16
|
+
*/
|
|
17
|
+
import { buildRecurrenceReport, } from './RecurrenceReader.js';
|
|
18
|
+
import { planActuation } from './RecurrenceActuator.js';
|
|
19
|
+
/** Read a store, converting failure into a named coverage gap rather than a throw. */
|
|
20
|
+
async function readOrRecord(store, read, into, coverage) {
|
|
21
|
+
try {
|
|
22
|
+
into.push(...(await read()));
|
|
23
|
+
coverage.read.push(store);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
coverage.unreadable.push({
|
|
27
|
+
store,
|
|
28
|
+
reason: err instanceof Error ? err.message.slice(0, 200) : String(err).slice(0, 200),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Run one pass: read → group → plan → create.
|
|
34
|
+
*
|
|
35
|
+
* Never throws for an unreadable store — that is data, reported in
|
|
36
|
+
* `report.coverage`, and it is what the actuator's refusal reads.
|
|
37
|
+
*/
|
|
38
|
+
export async function runRecurrenceLoop(deps, opts = {}) {
|
|
39
|
+
const observations = [];
|
|
40
|
+
const coverage = { read: [], unreadable: [], completeness: 'complete' };
|
|
41
|
+
await readOrRecord('attention', deps.readAttention, observations, coverage);
|
|
42
|
+
await readOrRecord('actions', deps.readActions, observations, coverage);
|
|
43
|
+
await readOrRecord('sentinel', deps.readSentinel, observations, coverage);
|
|
44
|
+
coverage.completeness = coverage.unreadable.length === 0 ? 'complete' : 'partial';
|
|
45
|
+
const report = buildRecurrenceReport(observations, coverage);
|
|
46
|
+
const plan = planActuation(report, opts);
|
|
47
|
+
if (plan.refused) {
|
|
48
|
+
return {
|
|
49
|
+
report,
|
|
50
|
+
created: [],
|
|
51
|
+
refused: plan.refused,
|
|
52
|
+
deferredByCap: plan.deferredByCap,
|
|
53
|
+
writeFailures: [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const created = [];
|
|
57
|
+
const writeFailures = [];
|
|
58
|
+
for (const proposal of plan.propose) {
|
|
59
|
+
try {
|
|
60
|
+
const { id } = await deps.createAction(proposal);
|
|
61
|
+
created.push({ id, title: proposal.title, observedCount: proposal.observedCount });
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
// A failed write is NOT a refusal. Recording it separately keeps a real
|
|
65
|
+
// outage from reading as a principled decision not to act — which would be
|
|
66
|
+
// this project's own failure mode (absence presenting as presence) at the
|
|
67
|
+
// very end of the loop it exists to close.
|
|
68
|
+
writeFailures.push({
|
|
69
|
+
title: proposal.title,
|
|
70
|
+
error: err instanceof Error ? err.message.slice(0, 200) : String(err).slice(0, 200),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { report, created, deferredByCap: plan.deferredByCap, writeFailures };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=recurrenceLoop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recurrenceLoop.js","sourceRoot":"","sources":["../../src/core/recurrenceLoop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,qBAAqB,GAItB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,aAAa,EAA8C,MAAM,yBAAyB,CAAC;AAiCpG,sFAAsF;AACtF,KAAK,UAAU,YAAY,CACzB,KAA2B,EAC3B,IAAiB,EACjB,IAAmB,EACnB,QAAkB;IAElB,IAAI,CAAC;QACH,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC;QAC7B,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC;YACvB,KAAK;YACL,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;SACrF,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAc,EACd,OAAyB,EAAE;IAE3B,MAAM,YAAY,GAAkB,EAAE,CAAC;IACvC,MAAM,QAAQ,GAAa,EAAE,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC;IAElF,MAAM,YAAY,CAAC,WAAW,EAAE,IAAI,CAAC,aAAa,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC;IAC5E,MAAM,YAAY,CAAC,SAAS,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC;IACxE,MAAM,YAAY,CAAC,UAAU,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC;IAC1E,QAAQ,CAAC,YAAY,GAAG,QAAQ,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;IAElF,MAAM,MAAM,GAAG,qBAAqB,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IAC7D,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAEzC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,OAAO;YACL,MAAM;YACN,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,aAAa,EAAE,EAAE;SAClB,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAA0B,EAAE,CAAC;IAC1C,MAAM,aAAa,GAAgC,EAAE,CAAC;IAEtD,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACpC,IAAI,CAAC;YACH,MAAM,EAAE,EAAE,EAAE,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;YACjD,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC;QACrF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,wEAAwE;YACxE,2EAA2E;YAC3E,0EAA0E;YAC1E,2CAA2C;YAC3C,aAAa,CAAC,IAAI,CAAC;gBACjB,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;aACpF,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,aAAa,EAAE,CAAC;AAC/E,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-07-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-07-27T04:10:39.708Z",
|
|
5
|
+
"instarVersion": "1.3.993",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The recurrence reader (shipped in the previous fragment) makes recurrence visible. Visibility was
|
|
9
|
+
never the goal: a report nobody acts on is the same filing-to-completion ratio with better
|
|
10
|
+
typography. Two modules close the remaining half.
|
|
11
|
+
|
|
12
|
+
`src/core/RecurrenceActuator.ts` turns a recurrence finding into a proposal for ONE tracked action on
|
|
13
|
+
the EXISTING evolution action queue — no new store, no new notification channel, no new authority.
|
|
14
|
+
Creating a tracked action queues work for a human or agent to judge; it does not close, prioritise,
|
|
15
|
+
escalate, or act on anything.
|
|
16
|
+
|
|
17
|
+
`src/core/recurrenceLoop.ts` is the caller that actually closes the loop: it reads the three stores,
|
|
18
|
+
plans, and writes through a caller-supplied action-creation function. It is deliberately the only
|
|
19
|
+
place in the feature that performs I/O, so a read failure has exactly one place to be reported from
|
|
20
|
+
and cannot be swallowed mid-pipeline.
|
|
21
|
+
|
|
22
|
+
## What to Tell Your User
|
|
23
|
+
|
|
24
|
+
Things you notice repeatedly can now become real work instead of being noticed again.
|
|
25
|
+
|
|
26
|
+
The part shipped earlier counted how often the same underlying problem had been raised. This part
|
|
27
|
+
takes the worst offenders and opens tracked items for them in the queue you already use — noticed
|
|
28
|
+
repeatedly, never owned, becomes a job that gets done or explicitly dropped. Dropping it is a fine
|
|
29
|
+
outcome, and the item says so: an explicit "no, we're fine with this" is worth far more than the same
|
|
30
|
+
thing being raised another two hundred times.
|
|
31
|
+
|
|
32
|
+
It is bounded on purpose. Out of 836 problems in a live dry run it would open three and hold back
|
|
33
|
+
seventeen, working through the backlog over time rather than replacing one pile with another.
|
|
34
|
+
|
|
35
|
+
The honest limit: nothing runs it on a schedule yet, so it closes the loop when something calls it,
|
|
36
|
+
not on its own.
|
|
37
|
+
|
|
38
|
+
## Summary of New Capabilities
|
|
39
|
+
|
|
40
|
+
No new endpoint, command, or config key. Two internal modules: one that decides what recurring,
|
|
41
|
+
untracked problem deserves a tracked action, and one that performs the read-plan-create pass through
|
|
42
|
+
the action queue that already exists.
|
|
43
|
+
|
|
44
|
+
## Evidence
|
|
45
|
+
|
|
46
|
+
Full loop run against the three live stores with only the write intercepted: coverage complete, 836
|
|
47
|
+
distinct problems, **would create 3, deferred 17 by the cap, zero write failures**. So "the loop
|
|
48
|
+
closes" is demonstrated, not designed.
|
|
49
|
+
|
|
50
|
+
Two refusals, deliberately **not** symmetric:
|
|
51
|
+
|
|
52
|
+
- **Actions store unreadable ⇒ propose nothing.** `tracked` is unknowable for every cluster, so every
|
|
53
|
+
cluster looks unowned and the actuator would manufacture duplicates of work that already exists —
|
|
54
|
+
the exact redundancy it was built to remove, under the banner of fixing it.
|
|
55
|
+
- **Attention or sentinel unreadable ⇒ proceed.** Those only understate counts, so a cluster still
|
|
56
|
+
clearing the threshold genuinely clears it. Treating all three identically would have looked
|
|
57
|
+
tidier and been wrong.
|
|
58
|
+
|
|
59
|
+
A **failed write** is recorded separately from a **refusal**. One is an outage; the other is a
|
|
60
|
+
decision not to act. Conflating them would let real breakage present as sound judgment. One failed
|
|
61
|
+
write does not abandon the remaining ones.
|
|
62
|
+
|
|
63
|
+
Bounds against the fix becoming its own pile: `minCount` (default 10), a hard per-run cap (default 3)
|
|
64
|
+
so it converges across sessions densest-first rather than turning 69 clusters into 69 items, and a
|
|
65
|
+
stable `externalKey` so a re-run updates instead of duplicating. Unit suite 30/30 across the feature;
|
|
66
|
+
`tsc --noEmit` exit 0.
|
|
67
|
+
|
|
68
|
+
## Known limits
|
|
69
|
+
|
|
70
|
+
**Nothing schedules it.** The loop can close unattended; nothing yet calls it, so today it closes only
|
|
71
|
+
when something invokes it. That is stated rather than implied. Priority is derived from volume alone
|
|
72
|
+
— deterministic, no model, no judgment — so a rare-but-serious problem ranks below a frequent-but-
|
|
73
|
+
trivial one. It inherits the reader's title-only keying, including its occasional over-merge.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Side-Effects Review — RecurrenceActuator (Tier 2 item 4, plan-only)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `recurrence-actuator`
|
|
4
|
+
**Date:** `2026-07-27`
|
|
5
|
+
**Author:** `Echo (instar-dev agent)`
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
`RecurrenceReader` makes recurrence visible. Visibility was never the goal — the project's diagnosis
|
|
10
|
+
is that instar notices constantly and closes almost nothing (≈30:1). A reader nobody acts on is that
|
|
11
|
+
ratio with better typography.
|
|
12
|
+
|
|
13
|
+
Operator directive 2026-07-26 20:08Z: *"the synthesis itself must lead to ACTION and a fully closed
|
|
14
|
+
loop"*, minimal user dependence.
|
|
15
|
+
|
|
16
|
+
`planActuation()` returns a PLAN: for clusters that genuinely recur AND are untracked, propose work on
|
|
17
|
+
the EXISTING evolution action queue. Pure — the caller performs the write, so the write path and its
|
|
18
|
+
gating stay exactly where they already are.
|
|
19
|
+
|
|
20
|
+
**Live dry-run, 2026-07-27:** 836 clusters considered → **3 proposed, 17 deferred by cap**
|
|
21
|
+
(278x idle-timeout, 238x escalation-suppressed, 177x credential rebalancer — all `high`).
|
|
22
|
+
|
|
23
|
+
## Refusal evidence (constraint 2)
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
REFUSAL 1 — actions store unreadable ⇒ propose NOTHING
|
|
27
|
+
refused.reason : actions-store-unreadable
|
|
28
|
+
detail : "…so 'has anyone already committed to this?' is unanswerable for every
|
|
29
|
+
cluster. Proposing work now would duplicate whatever is already tracked."
|
|
30
|
+
propose : [] consideredClusters: 1 ← still honest about scope
|
|
31
|
+
|
|
32
|
+
DELIBERATELY NOT SYMMETRIC — attention/sentinel unreadable ⇒ it DOES act
|
|
33
|
+
Those only UNDERSTATE counts, so a cluster still clearing the bar genuinely clears it.
|
|
34
|
+
Treating all partial reads alike would be lazy symmetry that blocks safe action.
|
|
35
|
+
|
|
36
|
+
REFUSAL 2 — per-run cap: 10 qualifying clusters ⇒ propose 3, deferredByCap 7
|
|
37
|
+
REFUSAL 3 — below threshold: seen 3x with minCount 10 ⇒ no-qualifying-clusters
|
|
38
|
+
REFUSAL 4 — already tracked: a member from the action queue ⇒ propose nothing
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Tests **10 passed (10)**; combined with the reader **21 passed (21)**; `tsc --noEmit` exit 0.
|
|
42
|
+
|
|
43
|
+
## A test caught the documented over-merge risk, live
|
|
44
|
+
|
|
45
|
+
My first cap test built clusters titled `problem 0`…`problem 9` and asserted 3 proposals. It got 1:
|
|
46
|
+
the recurrence key normalizes digits to `N`, so all ten collapsed to one cluster. **The fixture was
|
|
47
|
+
naive, not the code** — and it is a live demonstration of the over-merge trade the reader's
|
|
48
|
+
side-effects review names as its weakest point. Fixed with word-titles and the reason recorded in
|
|
49
|
+
the test.
|
|
50
|
+
|
|
51
|
+
## Decision-point inventory
|
|
52
|
+
|
|
53
|
+
| point | classification |
|
|
54
|
+
|---|---|
|
|
55
|
+
| actions-unreadable ⇒ refuse | `invariant` — the load-bearing rule |
|
|
56
|
+
| untracked + minCount filter | `invariant` — deterministic thresholds |
|
|
57
|
+
| per-run cap, densest-first | `invariant` |
|
|
58
|
+
| priority from volume | `invariant` — fixed bands, no model |
|
|
59
|
+
| `externalKey` from cluster key | `invariant` — stable, idempotent |
|
|
60
|
+
|
|
61
|
+
No judgment points, no LLM.
|
|
62
|
+
|
|
63
|
+
## 1. Over-block
|
|
64
|
+
|
|
65
|
+
Refuses entirely when the actions store is unreadable — deliberately, since acting blind creates
|
|
66
|
+
duplicates. Cost: on a broken actions store, nothing is proposed until it is fixed. Correct trade.
|
|
67
|
+
|
|
68
|
+
The `minCount` default of 10 will skip genuine problems seen 4–9 times. Accepted: a work item per
|
|
69
|
+
seen-twice observation is how the queue got to 371 open in the first place.
|
|
70
|
+
|
|
71
|
+
## 2. Under-block
|
|
72
|
+
|
|
73
|
+
**Nothing prevents the CALLER from ignoring the plan or writing it badly.** This module returns data;
|
|
74
|
+
the write is the caller's. That is the right seam (the write path keeps its own gating) but it means
|
|
75
|
+
"the loop closes" is only true once a caller is wired. No caller ships here — that is the next step
|
|
76
|
+
and is not claimed.
|
|
77
|
+
|
|
78
|
+
**Cancelled actions are not re-proposed-proof.** If a human cancels a proposed action, the cluster
|
|
79
|
+
remains untracked, so a later run could propose it again. The `externalKey` makes it the same row
|
|
80
|
+
rather than a new one, but a "dismissed, stop asking" state belongs to the action store, not here.
|
|
81
|
+
Named as a real gap. <!-- tracked: ACT-1311 -->
|
|
82
|
+
|
|
83
|
+
## 3. Level-of-abstraction fit
|
|
84
|
+
|
|
85
|
+
Plan-only, pure over the report. It cannot flood, cannot notify, cannot write. The one thing it
|
|
86
|
+
must never become — a fourth place that notices things — is structurally impossible: it has no
|
|
87
|
+
output channel.
|
|
88
|
+
|
|
89
|
+
## 4. Signal vs authority compliance
|
|
90
|
+
|
|
91
|
+
It proposes; it holds no authority. Creating a tracked action QUEUES work for a human or agent to
|
|
92
|
+
judge — it does not close, prioritise beyond a fixed volume rule, escalate, or act.
|
|
93
|
+
|
|
94
|
+
## 5. Interactions
|
|
95
|
+
|
|
96
|
+
Consumes `RecurrenceReport` only. No writes, no schema change, no existing caller. Depends on
|
|
97
|
+
`RecurrenceReader` (same branch, PR #1662) — this is stacked on it.
|
|
98
|
+
|
|
99
|
+
## 6. External surfaces
|
|
100
|
+
|
|
101
|
+
**None.** No route, no config, no persisted state, no user-visible behaviour in this increment.
|
|
102
|
+
|
|
103
|
+
## 7. Multi-machine posture
|
|
104
|
+
|
|
105
|
+
**Posture: `machine-local`.** `machine-local-justification: physical-credential-locality` — it plans
|
|
106
|
+
over one machine's stores, whose observation titles carry that machine's ids, topics and account
|
|
107
|
+
emails. Cross-machine synthesis would mean replicating those records; the correct route is the
|
|
108
|
+
existing pool-scope fan-out, serving each machine's data from that machine.
|
|
109
|
+
|
|
110
|
+
## 8. Rollback cost
|
|
111
|
+
|
|
112
|
+
**Zero.** One module, one test file, no callers. Delete removes the feature.
|
|
113
|
+
|
|
114
|
+
## Phase 5 — Second-pass review
|
|
115
|
+
|
|
116
|
+
No gate/sentinel/watchdog, no block/allow authority, no session lifecycle, no LLM. Author lenses:
|
|
117
|
+
|
|
118
|
+
**Adversarial — "how would I make this useless?"** Let it propose blind when the actions store is
|
|
119
|
+
down (duplicates existing work), or let it propose for everything at once (new backlog). Both are
|
|
120
|
+
asserted refusals.
|
|
121
|
+
|
|
122
|
+
**"Would it have caught the incident?"** The incident is 69 untracked recurrers. It selects exactly
|
|
123
|
+
that class and would open the top three today.
|
|
124
|
+
|
|
125
|
+
**"Symptom or cause?"** Cause for the never-gets-picked-up half. NOT for the recurrence itself —
|
|
126
|
+
proposing work does not fix the 278x idle-timeout; it makes someone decide about it. Claiming
|
|
127
|
+
otherwise would be filing-as-progress.
|
|
128
|
+
|
|
129
|
+
**Weakest point:** no caller is wired, so "the loop closes" is a designed property, not yet a
|
|
130
|
+
demonstrated one. The dry-run shows what it WOULD propose; nothing has been written.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Side-Effects Review — recurrenceLoop (the caller that closes the loop)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `recurrence-loop` · **Date:** `2026-07-27` · **Author:** `Echo (instar-dev agent)`
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
`RecurrenceReader` sees; `RecurrenceActuator` decides. Both are pure, which left **"the loop closes"
|
|
8
|
+
a DESIGNED property, not a demonstrated one** — the weakest point named in the actuator's own review.
|
|
9
|
+
This closes it: read three stores → group → plan → create, via a caller-supplied `createAction`.
|
|
10
|
+
|
|
11
|
+
It is deliberately the ONLY I/O in the feature, so every read failure has exactly one reporting site
|
|
12
|
+
and cannot be swallowed mid-pipeline.
|
|
13
|
+
|
|
14
|
+
**Live run, writes intercepted, 2026-07-27:** coverage `complete` (attention, actions, sentinel);
|
|
15
|
+
836 problems; **would create 3, deferred 17, 0 write failures, no refusal.**
|
|
16
|
+
|
|
17
|
+
## Refusal evidence (constraint 2)
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
REFUSAL — action store unreadable ⇒ createAction NEVER CALLED
|
|
21
|
+
create spy : not called created: 0
|
|
22
|
+
refused.reason : actions-store-unreadable writeFailures: 0
|
|
23
|
+
|
|
24
|
+
A FAILED WRITE IS NOT A REFUSAL (the distinction this module exists to protect)
|
|
25
|
+
createAction throws '503 action store unavailable'
|
|
26
|
+
refused : undefined ← an outage must NOT present as judgement
|
|
27
|
+
writeFailures : [{ error: '503 …' }]
|
|
28
|
+
created : 0
|
|
29
|
+
|
|
30
|
+
one failure among three ⇒ created 2, writeFailures 1 (others not abandoned)
|
|
31
|
+
|
|
32
|
+
UNREADABLE STORE IS DATA, NOT AN EXCEPTION
|
|
33
|
+
sentinel throws ⇒ coverage.unreadable names it, completeness 'partial',
|
|
34
|
+
and it STILL creates 1 (sentinel only understates counts)
|
|
35
|
+
all three throw ⇒ verdict undefined, created 0, no crash
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Tests **9 passed (9)**; whole feature **30 passed (30)**; `tsc --noEmit` exit 0.
|
|
39
|
+
|
|
40
|
+
## Decision-point inventory
|
|
41
|
+
|
|
42
|
+
| point | classification |
|
|
43
|
+
|---|---|
|
|
44
|
+
| read failure → coverage gap | `invariant` — try/catch → named entry, never a throw |
|
|
45
|
+
| refusal vs write-failure separation | `invariant` — distinct fields, the load-bearing rule |
|
|
46
|
+
| write loop continues past a failure | `invariant` |
|
|
47
|
+
|
|
48
|
+
No judgment points, no LLM, no authority: `createAction` is supplied by the caller, so the write path
|
|
49
|
+
keeps whatever gating it already has. This module never constructs an HTTP call.
|
|
50
|
+
|
|
51
|
+
## 1. Over-block
|
|
52
|
+
|
|
53
|
+
Refuses to write when the actions store is unreadable — inherited from the actuator and correct: it
|
|
54
|
+
cannot tell what is already owned. Cost: a broken actions store stops proposals until fixed.
|
|
55
|
+
|
|
56
|
+
## 2. Under-block
|
|
57
|
+
|
|
58
|
+
**No scheduler/route calls it.** Running it is deliberate. So this demonstrates the loop CAN close,
|
|
59
|
+
not that it closes *unattended*. Wiring a cadence is a separate increment with its own risk, and is
|
|
60
|
+
not claimed here.
|
|
61
|
+
|
|
62
|
+
**No dismissal memory.** A cancelled action leaves the cluster untracked, so a later run can propose
|
|
63
|
+
it again (same `externalKey`, so one row, not many). A "dismissed, stop asking" state belongs to the
|
|
64
|
+
action store. <!-- tracked: ACT-1311 -->
|
|
65
|
+
|
|
66
|
+
**`createAction` failures are reported, not retried.** Deliberate: retry policy belongs to the write
|
|
67
|
+
path, and a silent retry here would obscure the outage the separate field exists to surface.
|
|
68
|
+
|
|
69
|
+
## 3. Level-of-abstraction fit
|
|
70
|
+
|
|
71
|
+
All I/O in one place, everything else pure. The alternative — reads scattered through reader and
|
|
72
|
+
actuator — is exactly how a failed read becomes an empty result that reads as "nothing found".
|
|
73
|
+
|
|
74
|
+
## 4. Signal vs authority
|
|
75
|
+
|
|
76
|
+
Holds none. It executes a plan produced by deterministic rules and writes through a function the
|
|
77
|
+
caller owns.
|
|
78
|
+
|
|
79
|
+
## 5. Interactions
|
|
80
|
+
|
|
81
|
+
Consumes `RecurrenceReader` + `RecurrenceActuator` (same stack, PRs #1662 / actuator branch). Writes
|
|
82
|
+
only via the injected function. No schema change.
|
|
83
|
+
|
|
84
|
+
## 6. External surfaces
|
|
85
|
+
|
|
86
|
+
**None.** No route, no config, no persisted state of its own.
|
|
87
|
+
|
|
88
|
+
## 7. Multi-machine posture
|
|
89
|
+
|
|
90
|
+
**Posture: `machine-local`.** `machine-local-justification: physical-credential-locality` — it reads
|
|
91
|
+
one machine's stores, whose observation titles carry that machine's ids, topics and account emails,
|
|
92
|
+
and writes to that machine's action queue. Cross-machine synthesis would mean replicating those
|
|
93
|
+
records; the existing pool-scope fan-out is the correct route.
|
|
94
|
+
|
|
95
|
+
## 8. Rollback cost
|
|
96
|
+
|
|
97
|
+
**Zero.** One module, one test file, no callers.
|
|
98
|
+
|
|
99
|
+
## Phase 5 — Second-pass review
|
|
100
|
+
|
|
101
|
+
No gate/sentinel/watchdog, no block/allow authority, no LLM. Lenses:
|
|
102
|
+
|
|
103
|
+
**Adversarial — "how would I make this useless?"** Report a write outage as a refusal, so breakage
|
|
104
|
+
reads as judgement. Asserted against directly.
|
|
105
|
+
|
|
106
|
+
**"Would it have caught the incident?"** It IS the incident's remedy: 69 untracked recurrers, top
|
|
107
|
+
three actionable today.
|
|
108
|
+
|
|
109
|
+
**"Symptom or cause?"** Cause for never-picked-up. Not for the recurrence itself — creating an item
|
|
110
|
+
makes someone decide about the 278x idle-timeout; it does not fix it.
|
|
111
|
+
|
|
112
|
+
**Weakest point:** nothing schedules it. "Closes unattended" remains unclaimed.
|