instar 1.3.19 → 1.3.21
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/feedback-factory/processor/process.d.ts +26 -0
- package/dist/feedback-factory/processor/process.d.ts.map +1 -0
- package/dist/feedback-factory/processor/process.js +47 -0
- package/dist/feedback-factory/processor/process.js.map +1 -0
- package/dist/feedback-factory/processor/reopen.d.ts +39 -0
- package/dist/feedback-factory/processor/reopen.d.ts.map +1 -0
- package/dist/feedback-factory/processor/reopen.js +41 -0
- package/dist/feedback-factory/processor/reopen.js.map +1 -0
- package/dist/feedback-factory/store/FeedbackStore.d.ts +66 -0
- package/dist/feedback-factory/store/FeedbackStore.d.ts.map +1 -0
- package/dist/feedback-factory/store/FeedbackStore.js +88 -0
- package/dist/feedback-factory/store/FeedbackStore.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.20.md +23 -0
- package/upgrades/1.3.21.md +26 -0
- package/upgrades/side-effects/feedback-factory-reopen.md +31 -0
- package/upgrades/side-effects/feedback-factory-store-compose.md +36 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* process.ts — the feedback-factory processing composition.
|
|
3
|
+
*
|
|
4
|
+
* Ties the already-parity'd pure pieces together over a FeedbackStore, mirroring
|
|
5
|
+
* the reference's cmd_cluster (decide) + cmd_apply_clusters (write) run back-to-back:
|
|
6
|
+
*
|
|
7
|
+
* 1. read unprocessed items + active clusters from the store
|
|
8
|
+
* 2. clusterItems() decides merge/create per item (parity'd, increment 4)
|
|
9
|
+
* 3. apply each decision to the store (create / merge); when a merge carries a
|
|
10
|
+
* "possible regression" note, computeReopen() (parity'd, increment 8) decides
|
|
11
|
+
* the reopen and applyReopen writes it
|
|
12
|
+
* 4. mark each item processed; counters update for observability
|
|
13
|
+
*
|
|
14
|
+
* The decision logic is already proven equivalent to the reference; this is the
|
|
15
|
+
* orchestration over the injected store, verified by the Tier-2 integration test
|
|
16
|
+
* against InMemoryFeedbackStore.
|
|
17
|
+
*/
|
|
18
|
+
import type { ClusterResult } from './types.js';
|
|
19
|
+
import type { FeedbackStore, FeedbackMetrics } from '../store/FeedbackStore.js';
|
|
20
|
+
export interface ProcessResult {
|
|
21
|
+
results: ClusterResult[];
|
|
22
|
+
metrics: FeedbackMetrics;
|
|
23
|
+
}
|
|
24
|
+
/** Run one clustering+apply pass over the store. `now` injected for the reopen audit note. */
|
|
25
|
+
export declare function processUnprocessed(store: FeedbackStore, now: string): ProcessResult;
|
|
26
|
+
//# sourceMappingURL=process.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/processor/process.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEhF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,OAAO,EAAE,eAAe,CAAC;CAC1B;AAED,8FAA8F;AAC9F,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,CA2BnF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* process.ts — the feedback-factory processing composition.
|
|
3
|
+
*
|
|
4
|
+
* Ties the already-parity'd pure pieces together over a FeedbackStore, mirroring
|
|
5
|
+
* the reference's cmd_cluster (decide) + cmd_apply_clusters (write) run back-to-back:
|
|
6
|
+
*
|
|
7
|
+
* 1. read unprocessed items + active clusters from the store
|
|
8
|
+
* 2. clusterItems() decides merge/create per item (parity'd, increment 4)
|
|
9
|
+
* 3. apply each decision to the store (create / merge); when a merge carries a
|
|
10
|
+
* "possible regression" note, computeReopen() (parity'd, increment 8) decides
|
|
11
|
+
* the reopen and applyReopen writes it
|
|
12
|
+
* 4. mark each item processed; counters update for observability
|
|
13
|
+
*
|
|
14
|
+
* The decision logic is already proven equivalent to the reference; this is the
|
|
15
|
+
* orchestration over the injected store, verified by the Tier-2 integration test
|
|
16
|
+
* against InMemoryFeedbackStore.
|
|
17
|
+
*/
|
|
18
|
+
import { clusterItems } from './cluster.js';
|
|
19
|
+
import { computeReopen } from './reopen.js';
|
|
20
|
+
/** Run one clustering+apply pass over the store. `now` injected for the reopen audit note. */
|
|
21
|
+
export function processUnprocessed(store, now) {
|
|
22
|
+
const items = store.getUnprocessedFeedback();
|
|
23
|
+
const clusters = store.getActiveClusters();
|
|
24
|
+
const itemById = new Map(items.map((i) => [i.feedbackId, i]));
|
|
25
|
+
const results = clusterItems(items, clusters);
|
|
26
|
+
for (const r of results) {
|
|
27
|
+
const item = itemById.get(r.feedbackId);
|
|
28
|
+
if (!item)
|
|
29
|
+
continue;
|
|
30
|
+
if (r.action === 'create') {
|
|
31
|
+
store.upsertClusterFromItem(r.clusterId, item);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
store.mergeIntoCluster(r.clusterId, item);
|
|
35
|
+
// A merge that flagged a possible regression auto-reopens the cluster.
|
|
36
|
+
if (r.note && r.note.includes('possible regression')) {
|
|
37
|
+
const cluster = store.getCluster(r.clusterId);
|
|
38
|
+
if (cluster) {
|
|
39
|
+
store.applyReopen(r.clusterId, computeReopen(cluster, r.feedbackId, now));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
store.markProcessed(r.feedbackId, r.clusterId);
|
|
44
|
+
}
|
|
45
|
+
return { results, metrics: store.metrics() };
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process.js","sourceRoot":"","sources":["../../../src/feedback-factory/processor/process.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAS5C,8FAA8F;AAC9F,MAAM,UAAU,kBAAkB,CAAC,KAAoB,EAAE,GAAW;IAClE,MAAM,KAAK,GAAG,KAAK,CAAC,sBAAsB,EAAE,CAAC;IAC7C,MAAM,QAAQ,GAAG,KAAK,CAAC,iBAAiB,EAAE,CAAC;IAC3C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAE9D,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAE9C,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACxC,IAAI,CAAC,IAAI;YAAE,SAAS;QAEpB,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC1B,KAAK,CAAC,qBAAqB,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACjD,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC1C,uEAAuE;YACvE,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC;gBACrD,MAAM,OAAO,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;gBAC9C,IAAI,OAAO,EAAE,CAAC;oBACZ,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,aAAa,CAAC,OAAO,EAAE,CAAC,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC;gBAC5E,CAAC;YACH,CAAC;QACH,CAAC;QACD,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;IACjD,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reopen.ts — TS port of the auto-reopen-on-regression decision.
|
|
3
|
+
*
|
|
4
|
+
* Ports the decision embedded in cmd_apply_clusters (the-portal/.claude/scripts/
|
|
5
|
+
* feedback-processor.py, the regression branch ~:1543) out of the DB-write code
|
|
6
|
+
* into a pure function. When the clustering driver (cluster.ts) merges a new
|
|
7
|
+
* report into a cluster that was already fixed/resolved/deferred (a "possible
|
|
8
|
+
* regression" note), the cluster is auto-reopened. This computes the reopen
|
|
9
|
+
* outcome from the cluster's prior status:
|
|
10
|
+
*
|
|
11
|
+
* - deferred → AGED-REOPEN → status 'new', annotate actionTaken, no recurrence bump
|
|
12
|
+
* - otherwise → REGRESSION → status 'investigating', annotate researchNotes, bump recurrenceCount
|
|
13
|
+
*
|
|
14
|
+
* The DB writes (the prisma update with `{ increment: 1 }` etc.) stay in the
|
|
15
|
+
* adapter; this is the pure decision + the audit-note text. `now` is injected.
|
|
16
|
+
* Reference is interleaved with DB I/O, so equivalence is by faithful
|
|
17
|
+
* transcription + exhaustive unit tests (no clean isolated reference function to
|
|
18
|
+
* run a cross-runtime harness against).
|
|
19
|
+
*/
|
|
20
|
+
import type { Cluster } from './types.js';
|
|
21
|
+
export interface ReopenDecision {
|
|
22
|
+
/** New lifecycle status to set on the reopened cluster. */
|
|
23
|
+
newStatus: 'new' | 'investigating';
|
|
24
|
+
/** Whether to bump recurrenceCount (chronic-regression accounting; NOT for aged-reopen). */
|
|
25
|
+
bumpRecurrence: boolean;
|
|
26
|
+
/** Which cluster field the audit note is appended to. */
|
|
27
|
+
annotateField: 'actionTaken' | 'researchNotes';
|
|
28
|
+
/** The note tag. */
|
|
29
|
+
noteTag: 'AGED-REOPEN' | 'REGRESSION';
|
|
30
|
+
/** The audit note appended to the chosen field. */
|
|
31
|
+
note: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Compute the auto-reopen outcome for a cluster being merged into on a regression.
|
|
35
|
+
* `wasStatus` is the cluster's status before reopen; `feedbackId` is the incoming
|
|
36
|
+
* report; `now` is the injected ISO timestamp (reference uses new Date().toISOString()).
|
|
37
|
+
*/
|
|
38
|
+
export declare function computeReopen(cluster: Cluster, feedbackId: string, now: string): ReopenDecision;
|
|
39
|
+
//# sourceMappingURL=reopen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reopen.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/processor/reopen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,SAAS,EAAE,KAAK,GAAG,eAAe,CAAC;IACnC,4FAA4F;IAC5F,cAAc,EAAE,OAAO,CAAC;IACxB,yDAAyD;IACzD,aAAa,EAAE,aAAa,GAAG,eAAe,CAAC;IAC/C,oBAAoB;IACpB,OAAO,EAAE,aAAa,GAAG,YAAY,CAAC;IACtC,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,cAAc,CAiB/F"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reopen.ts — TS port of the auto-reopen-on-regression decision.
|
|
3
|
+
*
|
|
4
|
+
* Ports the decision embedded in cmd_apply_clusters (the-portal/.claude/scripts/
|
|
5
|
+
* feedback-processor.py, the regression branch ~:1543) out of the DB-write code
|
|
6
|
+
* into a pure function. When the clustering driver (cluster.ts) merges a new
|
|
7
|
+
* report into a cluster that was already fixed/resolved/deferred (a "possible
|
|
8
|
+
* regression" note), the cluster is auto-reopened. This computes the reopen
|
|
9
|
+
* outcome from the cluster's prior status:
|
|
10
|
+
*
|
|
11
|
+
* - deferred → AGED-REOPEN → status 'new', annotate actionTaken, no recurrence bump
|
|
12
|
+
* - otherwise → REGRESSION → status 'investigating', annotate researchNotes, bump recurrenceCount
|
|
13
|
+
*
|
|
14
|
+
* The DB writes (the prisma update with `{ increment: 1 }` etc.) stay in the
|
|
15
|
+
* adapter; this is the pure decision + the audit-note text. `now` is injected.
|
|
16
|
+
* Reference is interleaved with DB I/O, so equivalence is by faithful
|
|
17
|
+
* transcription + exhaustive unit tests (no clean isolated reference function to
|
|
18
|
+
* run a cross-runtime harness against).
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Compute the auto-reopen outcome for a cluster being merged into on a regression.
|
|
22
|
+
* `wasStatus` is the cluster's status before reopen; `feedbackId` is the incoming
|
|
23
|
+
* report; `now` is the injected ISO timestamp (reference uses new Date().toISOString()).
|
|
24
|
+
*/
|
|
25
|
+
export function computeReopen(cluster, feedbackId, now) {
|
|
26
|
+
const wasStatus = cluster.status ?? null;
|
|
27
|
+
const isAged = wasStatus === 'deferred';
|
|
28
|
+
const newStatus = isAged ? 'new' : 'investigating';
|
|
29
|
+
const noteTag = isAged ? 'AGED-REOPEN' : 'REGRESSION';
|
|
30
|
+
const fixedInVersion = cluster.fixedInVersion || 'n/a';
|
|
31
|
+
// Verbatim from the reference's regressionNote template.
|
|
32
|
+
const note = `[${noteTag} ${now}] New report matched cluster previously marked '${wasStatus}' (fixedInVersion=${fixedInVersion}). Auto-reopened to '${newStatus}' for review. Report: ${feedbackId}`;
|
|
33
|
+
return {
|
|
34
|
+
newStatus,
|
|
35
|
+
bumpRecurrence: !isAged,
|
|
36
|
+
annotateField: isAged ? 'actionTaken' : 'researchNotes',
|
|
37
|
+
noteTag,
|
|
38
|
+
note,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=reopen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reopen.js","sourceRoot":"","sources":["../../../src/feedback-factory/processor/reopen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAiBH;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,OAAgB,EAAE,UAAkB,EAAE,GAAW;IAC7E,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC;IACzC,MAAM,MAAM,GAAG,SAAS,KAAK,UAAU,CAAC;IACxC,MAAM,SAAS,GAA4B,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC;IAC5E,MAAM,OAAO,GAAiC,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC;IACpF,MAAM,cAAc,GAAI,OAAO,CAAC,cAAyB,IAAI,KAAK,CAAC;IAEnE,yDAAyD;IACzD,MAAM,IAAI,GAAG,IAAI,OAAO,IAAI,GAAG,mDAAmD,SAAS,qBAAqB,cAAc,wBAAwB,SAAS,yBAAyB,UAAU,EAAE,CAAC;IAErM,OAAO;QACL,SAAS;QACT,cAAc,EAAE,CAAC,MAAM;QACvB,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,eAAe;QACvD,OAAO;QACP,IAAI;KACL,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeedbackStore.ts — the data-access seam for the feedback factory.
|
|
3
|
+
*
|
|
4
|
+
* The ported processor logic (fingerprint / similarity / cluster / verify /
|
|
5
|
+
* reopen) is pure; the *stateful* operations (read unprocessed items, read active
|
|
6
|
+
* clusters, create/merge/reopen clusters, mark processed, count) live behind this
|
|
7
|
+
* interface so the canonical instance's real adapter (Prisma cloud DB — the
|
|
8
|
+
* blocked, credentials-gated piece) is a thin shim, while everything is buildable
|
|
9
|
+
* and testable today against `InMemoryFeedbackStore`.
|
|
10
|
+
*
|
|
11
|
+
* This is the dependency-injection boundary called out in the migration spec:
|
|
12
|
+
* front → Vercel, processor → Instar job, DB → cloud (Prisma). The interface
|
|
13
|
+
* shape is driven by what the ported drivers + the composition (process.ts) need.
|
|
14
|
+
*/
|
|
15
|
+
import type { FeedbackItem, Cluster, ClusterResult } from '../processor/types.js';
|
|
16
|
+
import type { ReopenDecision } from '../processor/reopen.js';
|
|
17
|
+
/** Read-only observability counters for the capture→cluster→reopen loop (spec §2.7). */
|
|
18
|
+
export interface FeedbackMetrics {
|
|
19
|
+
captured: number;
|
|
20
|
+
created: number;
|
|
21
|
+
merged: number;
|
|
22
|
+
reopened: number;
|
|
23
|
+
}
|
|
24
|
+
export interface FeedbackStore {
|
|
25
|
+
/** Unprocessed feedback, oldest first (mirrors the reference's `status:'unprocessed'` query). */
|
|
26
|
+
getUnprocessedFeedback(): FeedbackItem[];
|
|
27
|
+
/** Active clusters (status != 'resolved'), the merge candidates. */
|
|
28
|
+
getActiveClusters(): Cluster[];
|
|
29
|
+
getCluster(clusterId: string): Cluster | undefined;
|
|
30
|
+
/** Create a new cluster from an item (or bump reportCount if it already exists). */
|
|
31
|
+
upsertClusterFromItem(clusterId: string, item: FeedbackItem): void;
|
|
32
|
+
/** Merge an item into an existing cluster (bump reportCount). */
|
|
33
|
+
mergeIntoCluster(clusterId: string, item: FeedbackItem): void;
|
|
34
|
+
/** Apply an auto-reopen decision (status, recurrence bump, audit-note append). */
|
|
35
|
+
applyReopen(clusterId: string, decision: ReopenDecision): void;
|
|
36
|
+
/** Mark a feedback item processed + linked to its cluster. */
|
|
37
|
+
markProcessed(feedbackId: string, clusterId: string): void;
|
|
38
|
+
metrics(): FeedbackMetrics;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* In-memory FeedbackStore — the test/reference implementation. Faithfully mirrors
|
|
42
|
+
* the reference's create/merge/reopen field mutations (reportCount increment,
|
|
43
|
+
* recurrenceCount bump on regression, status change, note append) so the
|
|
44
|
+
* composition can be integration-tested without a database.
|
|
45
|
+
*/
|
|
46
|
+
export declare class InMemoryFeedbackStore implements FeedbackStore {
|
|
47
|
+
private feedback;
|
|
48
|
+
private clusters;
|
|
49
|
+
private counts;
|
|
50
|
+
constructor(seed?: {
|
|
51
|
+
feedback?: FeedbackItem[];
|
|
52
|
+
clusters?: Cluster[];
|
|
53
|
+
});
|
|
54
|
+
getUnprocessedFeedback(): FeedbackItem[];
|
|
55
|
+
getActiveClusters(): Cluster[];
|
|
56
|
+
getCluster(clusterId: string): Cluster | undefined;
|
|
57
|
+
upsertClusterFromItem(clusterId: string, item: FeedbackItem): void;
|
|
58
|
+
mergeIntoCluster(clusterId: string, _item: FeedbackItem): void;
|
|
59
|
+
applyReopen(clusterId: string, decision: ReopenDecision): void;
|
|
60
|
+
markProcessed(feedbackId: string, clusterId: string): void;
|
|
61
|
+
metrics(): FeedbackMetrics;
|
|
62
|
+
/** Test helper: snapshot all clusters. */
|
|
63
|
+
allClusters(): Cluster[];
|
|
64
|
+
}
|
|
65
|
+
export type { ClusterResult };
|
|
66
|
+
//# sourceMappingURL=FeedbackStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeedbackStore.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/store/FeedbackStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,wFAAwF;AACxF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,sBAAsB,IAAI,YAAY,EAAE,CAAC;IACzC,oEAAoE;IACpE,iBAAiB,IAAI,OAAO,EAAE,CAAC;IAC/B,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;IACnD,oFAAoF;IACpF,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;IACnE,iEAAiE;IACjE,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9D,kFAAkF;IAClF,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;IAC/D,8DAA8D;IAC9D,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3D,OAAO,IAAI,eAAe,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IACzD,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,MAAM,CAAwE;gBAE1E,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;KAAE;IAKtE,sBAAsB,IAAI,YAAY,EAAE;IAMxC,iBAAiB,IAAI,OAAO,EAAE;IAI9B,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAIlD,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI;IAYlE,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI;IAM9D,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,IAAI;IAW9D,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAM1D,OAAO,IAAI,eAAe;IAI1B,0CAA0C;IAC1C,WAAW,IAAI,OAAO,EAAE;CAGzB;AAED,YAAY,EAAE,aAAa,EAAE,CAAC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeedbackStore.ts — the data-access seam for the feedback factory.
|
|
3
|
+
*
|
|
4
|
+
* The ported processor logic (fingerprint / similarity / cluster / verify /
|
|
5
|
+
* reopen) is pure; the *stateful* operations (read unprocessed items, read active
|
|
6
|
+
* clusters, create/merge/reopen clusters, mark processed, count) live behind this
|
|
7
|
+
* interface so the canonical instance's real adapter (Prisma cloud DB — the
|
|
8
|
+
* blocked, credentials-gated piece) is a thin shim, while everything is buildable
|
|
9
|
+
* and testable today against `InMemoryFeedbackStore`.
|
|
10
|
+
*
|
|
11
|
+
* This is the dependency-injection boundary called out in the migration spec:
|
|
12
|
+
* front → Vercel, processor → Instar job, DB → cloud (Prisma). The interface
|
|
13
|
+
* shape is driven by what the ported drivers + the composition (process.ts) need.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* In-memory FeedbackStore — the test/reference implementation. Faithfully mirrors
|
|
17
|
+
* the reference's create/merge/reopen field mutations (reportCount increment,
|
|
18
|
+
* recurrenceCount bump on regression, status change, note append) so the
|
|
19
|
+
* composition can be integration-tested without a database.
|
|
20
|
+
*/
|
|
21
|
+
export class InMemoryFeedbackStore {
|
|
22
|
+
feedback = new Map();
|
|
23
|
+
clusters = new Map();
|
|
24
|
+
counts = { captured: 0, created: 0, merged: 0, reopened: 0 };
|
|
25
|
+
constructor(seed) {
|
|
26
|
+
for (const f of seed?.feedback ?? [])
|
|
27
|
+
this.feedback.set(f.feedbackId, { status: 'unprocessed', ...f });
|
|
28
|
+
for (const c of seed?.clusters ?? [])
|
|
29
|
+
this.clusters.set(c.clusterId, { ...c });
|
|
30
|
+
}
|
|
31
|
+
getUnprocessedFeedback() {
|
|
32
|
+
return [...this.feedback.values()]
|
|
33
|
+
.filter((f) => (f.status ?? 'unprocessed') === 'unprocessed')
|
|
34
|
+
.sort((a, b) => String(a.receivedAt ?? '').localeCompare(String(b.receivedAt ?? '')));
|
|
35
|
+
}
|
|
36
|
+
getActiveClusters() {
|
|
37
|
+
return [...this.clusters.values()].filter((c) => c.status !== 'resolved');
|
|
38
|
+
}
|
|
39
|
+
getCluster(clusterId) {
|
|
40
|
+
return this.clusters.get(clusterId);
|
|
41
|
+
}
|
|
42
|
+
upsertClusterFromItem(clusterId, item) {
|
|
43
|
+
const existing = this.clusters.get(clusterId);
|
|
44
|
+
if (existing) {
|
|
45
|
+
existing.reportCount = (existing.reportCount ?? 0) + 1;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
this.clusters.set(clusterId, {
|
|
49
|
+
clusterId, title: item.title, description: item.description, type: item.type, reportCount: 1,
|
|
50
|
+
});
|
|
51
|
+
this.counts.created++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
mergeIntoCluster(clusterId, _item) {
|
|
55
|
+
const c = this.clusters.get(clusterId);
|
|
56
|
+
if (c)
|
|
57
|
+
c.reportCount = (c.reportCount ?? 0) + 1;
|
|
58
|
+
this.counts.merged++;
|
|
59
|
+
}
|
|
60
|
+
applyReopen(clusterId, decision) {
|
|
61
|
+
const c = this.clusters.get(clusterId);
|
|
62
|
+
if (!c)
|
|
63
|
+
return;
|
|
64
|
+
c.status = decision.newStatus;
|
|
65
|
+
if (decision.bumpRecurrence)
|
|
66
|
+
c.recurrenceCount = (c.recurrenceCount ?? 0) + 1;
|
|
67
|
+
const field = decision.annotateField;
|
|
68
|
+
const prior = c[field] ? `${c[field]}\n\n` : '';
|
|
69
|
+
c[field] = prior + decision.note;
|
|
70
|
+
this.counts.reopened++;
|
|
71
|
+
}
|
|
72
|
+
markProcessed(feedbackId, clusterId) {
|
|
73
|
+
const f = this.feedback.get(feedbackId);
|
|
74
|
+
if (f) {
|
|
75
|
+
f.status = 'processing';
|
|
76
|
+
f.clusterId = clusterId;
|
|
77
|
+
}
|
|
78
|
+
this.counts.captured++;
|
|
79
|
+
}
|
|
80
|
+
metrics() {
|
|
81
|
+
return { ...this.counts };
|
|
82
|
+
}
|
|
83
|
+
/** Test helper: snapshot all clusters. */
|
|
84
|
+
allClusters() {
|
|
85
|
+
return [...this.clusters.values()].map((c) => ({ ...c }));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=FeedbackStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeedbackStore.js","sourceRoot":"","sources":["../../../src/feedback-factory/store/FeedbackStore.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-05-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-05-27T08:23:24.754Z",
|
|
5
|
+
"instarVersion": "1.3.21",
|
|
6
6
|
"entryCount": 192,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
Eighth increment of the **Feedback Factory Migration** (Dawn → Echo; spec `docs/specs/feedback-factory-migration.md`, approved). Ports the **auto-reopen-on-regression decision** from the reference Python (`cmd_apply_clusters` in `the-portal/.claude/scripts/feedback-processor.py`) into a pure function at `src/feedback-factory/processor/reopen.ts`.
|
|
8
|
+
|
|
9
|
+
When a new report gets grouped onto a bug that was already marked fixed, resolved, or deferred, the bug is automatically reopened for review. This computes how: a long-deferred bug becomes "new" again (an aged-reopen, logged on its action trail); a fixed/resolved bug becomes "investigating" again and bumps its recurrence counter (a regression, logged on its research notes). It also writes the exact audit note. Pure function; **not wired into any route or job yet** — no behavioral change.
|
|
10
|
+
|
|
11
|
+
## What to Tell Your User
|
|
12
|
+
|
|
13
|
+
- The safety behavior that catches a bug "coming back from the dead" — a report landing on an already-fixed bug auto-reopens it instead of silently swallowing it — is now ported, including the audit note that records why it reopened.
|
|
14
|
+
|
|
15
|
+
## Summary of New Capabilities
|
|
16
|
+
|
|
17
|
+
| Capability | How to Use |
|
|
18
|
+
|-----------|-----------|
|
|
19
|
+
| Auto-reopen decision (TS port) | Internal module `src/feedback-factory/processor/reopen.ts` — not yet wired |
|
|
20
|
+
|
|
21
|
+
## Evidence
|
|
22
|
+
|
|
23
|
+
- The decision is interleaved with database writes in the reference, so equivalence is by faithful transcription plus both-sides-of-boundary unit tests (5): the deferred (aged-reopen) branch vs the fixed/resolved (regression) branch — each asserting the right new status, which field gets annotated, and whether the recurrence counter bumps — plus the audit-note string asserted verbatim and the `fixedInVersion=n/a` fallback.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
|
|
5
|
+
## What Changed
|
|
6
|
+
|
|
7
|
+
Ninth increment of the **Feedback Factory Migration** (Dawn → Echo; spec `docs/specs/feedback-factory-migration.md`, approved) — the piece that assembles the ported brain. Adds the data-access seam (`FeedbackStore` interface + an in-memory implementation) at `src/feedback-factory/store/`, the processing composition `processUnprocessed` at `src/feedback-factory/processor/process.ts` (which wires the already-ported clustering + auto-reopen together, mirroring the reference's "decide then apply" steps), and the factory's own observability counters (captured / created / merged / reopened).
|
|
8
|
+
|
|
9
|
+
The real database adapter is a thin shim that implements the same interface — left for cutover since it needs cloud credentials and the hosting decision. **Nothing is wired into a route or job yet** — no behavioral change.
|
|
10
|
+
|
|
11
|
+
## What to Tell Your User
|
|
12
|
+
|
|
13
|
+
- The individual pieces of the feedback brain now snap together into one working pipeline (read new reports → group them → reopen regressions → count what happened), proven end-to-end against an in-memory test database.
|
|
14
|
+
- It also keeps its own running tally — how many reports came in, how many new bug-piles were created, how many merged, how many reopened — the start of the factory's self-monitoring.
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
| Capability | How to Use |
|
|
19
|
+
|-----------|-----------|
|
|
20
|
+
| FeedbackStore seam + in-memory store | `src/feedback-factory/store/FeedbackStore.ts` — the DB-adapter boundary (real adapter at cutover) |
|
|
21
|
+
| Processing composition | `processUnprocessed(store, now)` in `src/feedback-factory/processor/process.ts` — not yet wired |
|
|
22
|
+
| Observability counters | `store.metrics()` — captured / created / merged / reopened |
|
|
23
|
+
|
|
24
|
+
## Evidence
|
|
25
|
+
|
|
26
|
+
- The decision logic inside (clustering, auto-reopen) is each already proven identical to Dawn's original in earlier increments. This increment adds the integration-level proof that they **compose** correctly: a Tier-2 test runs the whole pipeline over a real in-memory store and asserts new clusters are created, duplicates merge (order-dependent), the false-merge guard blocks a mid-similarity merge into a fixed bug, a genuine regression auto-reopens (status → investigating, recurrence bumped, audit note written), the counters tally, and empty input is a clean no-op.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Side-Effects Review — auto-reopen-on-regression decision (Phase 1, increment 8)
|
|
2
|
+
|
|
3
|
+
**Slug:** `feedback-factory-reopen`
|
|
4
|
+
**Date:** `2026-05-27`
|
|
5
|
+
**Author:** Echo (autonomous)
|
|
6
|
+
**Spec:** `docs/specs/feedback-factory-migration.md` (converged v2, approved by Justin 2026-05-26)
|
|
7
|
+
**Scope:** The pure auto-reopen DECISION (which status/field/recurrence-bump + audit note) extracted from the regression branch of `cmd_apply_clusters`. The prisma writes stay in the (later) store adapter.
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
Ports the auto-reopen-on-regression decision into `src/feedback-factory/processor/reopen.ts` as the pure `computeReopen(cluster, feedbackId, now)`. When the clustering driver merges a report into a fixed/resolved/deferred cluster (a "possible regression"), the cluster is auto-reopened: `deferred → AGED-REOPEN → status 'new', annotate actionTaken, no recurrence bump`; otherwise `REGRESSION → status 'investigating', annotate researchNotes, bump recurrenceCount`. Also templates the audit note verbatim. `now` injected. **Not wired into any route/job yet.**
|
|
12
|
+
|
|
13
|
+
## Equivalence verification
|
|
14
|
+
|
|
15
|
+
The decision is interleaved with DB writes in the reference, so equivalence is by faithful transcription + both-sides-of-boundary unit tests (5): deferred vs fixed vs resolved → correct status/field/tag/recurrence; the audit-note string asserted verbatim (tag, time, prior status, `fixedInVersion`, new status, report id); `fixedInVersion=n/a` fallback.
|
|
16
|
+
|
|
17
|
+
## Seven-dimension review
|
|
18
|
+
|
|
19
|
+
1. **Over/under-reach** — Pure function, no I/O, no state, not imported by any runtime path. Returns the decision + note; the caller (store adapter) does the prisma update.
|
|
20
|
+
2. **Level-of-abstraction fit** — Processor-logic layer, alongside cluster/transition/verify. The DB write (`{ increment: 1 }`, field append) is correctly left to the adapter.
|
|
21
|
+
3. **Signal vs Authority** — N/A; produces a decision. The reopen is itself a legitimate guard (regression must not stay marked fixed) that exists in the reference.
|
|
22
|
+
4. **Interactions** — None. New isolated module; nothing imports it yet. Pairs conceptually with `cluster.ts` (which emits the regression merge-note) but no code coupling yet.
|
|
23
|
+
5. **Rollback cost** — Trivial: delete the module + tests.
|
|
24
|
+
6. **Migration parity** — N/A. New internal library code; no agent-installed file touched.
|
|
25
|
+
7. **Failure modes** — (a) Wrong field/status/recurrence per prior-status → both-sides tests. (b) Audit-note drift → asserted verbatim. (c) `now` non-determinism → injected.
|
|
26
|
+
|
|
27
|
+
## Tests
|
|
28
|
+
|
|
29
|
+
- Tier-1 unit (CI): `tests/unit/feedback-factory/reopen.test.ts` — 5 tests (aged vs regression branches, resolved, verbatim note, n/a fallback).
|
|
30
|
+
- No cross-runtime parity harness (decision interleaved with DB writes in the reference; equivalence by transcription + boundary tests).
|
|
31
|
+
- No integration/E2E this increment: not wired; attaches when the apply/store layer lands. Reasoned, documented.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Side-Effects Review — store seam + processing composition + observability (Phase 1, increment 9)
|
|
2
|
+
|
|
3
|
+
**Slug:** `feedback-factory-store-compose`
|
|
4
|
+
**Date:** `2026-05-27`
|
|
5
|
+
**Author:** Echo (autonomous)
|
|
6
|
+
**Spec:** `docs/specs/feedback-factory-migration.md` (converged v2, approved by Justin 2026-05-26)
|
|
7
|
+
**Scope:** The data-access seam (`FeedbackStore` interface + `InMemoryFeedbackStore`), the processing composition (`processUnprocessed` = clusterItems + computeReopen over the store), and the observability counters. The REAL Prisma adapter + the HTTP/app placement remain the blocked, credentials/decision-gated pieces.
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
Adds:
|
|
12
|
+
- `src/feedback-factory/store/FeedbackStore.ts` — the dependency-injection boundary (interface + `FeedbackMetrics` counters + `InMemoryFeedbackStore`).
|
|
13
|
+
- `src/feedback-factory/processor/process.ts` — `processUnprocessed(store, now)`, which composes the already-parity'd pieces, mirroring the reference's `cmd_cluster` (decide) + `cmd_apply_clusters` (write) run back-to-back: read unprocessed + active clusters → `clusterItems` decides → apply create/merge → on a "possible regression" merge, `computeReopen` + `applyReopen` → mark processed → update counters.
|
|
14
|
+
- Observability: the `FeedbackMetrics` surface (captured / created / merged / reopened) the store maintains (spec §2.7's "meter the loop", minimal form).
|
|
15
|
+
|
|
16
|
+
This is the glue that assembles the ported brain. The real Prisma adapter is a thin shim that implements the same interface; **nothing is wired into a route/job yet** — no behavioral change.
|
|
17
|
+
|
|
18
|
+
## Equivalence / verification
|
|
19
|
+
|
|
20
|
+
The constituent decision logic (`clusterItems`, `computeReopen`) is each already parity-verified against the reference (increments 4 + 8). This increment adds the **Tier-2 integration test** proving they COMPOSE correctly over the data seam: new clusters created, duplicates merged (order-dependent), the 0.55 false-merge guard blocks a mid-similarity merge into a fixed cluster, a real regression auto-reopens (status → investigating, recurrence bump, audit note), counters tally, and empty input is a no-op. `InMemoryFeedbackStore`'s field mutations (reportCount increment, recurrence bump on regression, note append with blank-line separator) mirror the reference's prisma `update` shapes.
|
|
21
|
+
|
|
22
|
+
## Seven-dimension review
|
|
23
|
+
|
|
24
|
+
1. **Over/under-reach** — Pure logic + an in-memory store; not wired to any route/job. The interface is shaped by what the ported drivers + composition need (not speculative). `InMemoryFeedbackStore` mutates only its own Maps.
|
|
25
|
+
2. **Level-of-abstraction fit** — `store/` is the data-access layer; `process.ts` is the composition; the real DB adapter (Prisma) implements the interface later. Correct seam — the blocked app/DB decisions sit cleanly behind it.
|
|
26
|
+
3. **Signal vs Authority** — The composition applies the decisions the parity'd pieces produce; the evidence gate / curated lifecycle still hold terminal authority (unchanged). Counters are read-only observability.
|
|
27
|
+
4. **Interactions** — First module to IMPORT the others (`cluster`, `reopen`, `types`) — but only in `process.ts`, which nothing else imports yet. No runtime path touched.
|
|
28
|
+
5. **Rollback cost** — Trivial: delete the store + process + tests. Additive.
|
|
29
|
+
6. **Migration parity** — N/A. New internal library code; no agent-installed file touched. (The real-adapter wiring + sender repoint are the separate Migration-Parity'd cutover steps, blocked.)
|
|
30
|
+
7. **Failure modes** — (a) Composition mis-wires create vs merge vs reopen → the Tier-2 integration test covers all branches. (b) Order-dependence lost → integration test asserts fb-2 merges fb-1's fresh cluster. (c) Reopen not triggered on regression → asserted (status/recurrence/note). (d) Counter drift → asserted per-branch + empty-input no-op.
|
|
31
|
+
|
|
32
|
+
## Tests
|
|
33
|
+
|
|
34
|
+
- Tier-1 unit (CI): `tests/unit/feedback-factory/store.test.ts` — 6 tests (read filters, create/merge/reopen mutations, aged vs regression recurrence).
|
|
35
|
+
- **Tier-2 integration (CI): `tests/integration/feedback-factory-process.test.ts` — 4 tests** composing the full pipeline over the real `InMemoryFeedbackStore` (the integration tier the pure-logic increments deferred until a store existed).
|
|
36
|
+
- E2E (feature-alive) attaches when the processor job + canonical front are wired — that's gated on the blocked app-placement + Prisma-adapter decisions. Reasoned, documented.
|