instar 1.3.20 → 1.3.22

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.
@@ -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,42 @@
1
+ /**
2
+ * reportPartition.ts — TS port of scar (d): lifecycle partitioning + re-report
3
+ * (cycling) prevention for the operator digest.
4
+ *
5
+ * Ports the pure partition decision from the report-generation function (:2747) of
6
+ * the reference `the-portal/.claude/scripts/feedback-processor.py` — the half of
7
+ * scar (d) that complements the cluster-level cycling detection already ported in
8
+ * transitions.ts. Given the current clusters + what was surfaced in the LAST
9
+ * operator report, it decides what is actionable now: newly-open vs already-known
10
+ * issues, the same for investigating, and items FIXED since the last report that
11
+ * haven't been announced before (the re-report/cycling guard — a fix is announced
12
+ * once, never again). It also decides whether the whole report should be skipped
13
+ * (nothing new ⇒ no noise).
14
+ *
15
+ * Pure: the clusters, the previous-report state, and `now` are injected (the real
16
+ * reporter does the DB query + state load + Telegram render around this). `now` is
17
+ * an ISO string. Equivalence is by faithful transcription + both-sides-of-boundary
18
+ * unit tests (the decision is embedded in a Telegram-rendering function in the
19
+ * reference, so there is no clean isolated function for a cross-runtime harness).
20
+ */
21
+ import type { Cluster } from './types.js';
22
+ export interface PreviousReportState {
23
+ lastReportAt?: string;
24
+ reportedOpenIds?: string[];
25
+ reportedInvestigatingIds?: string[];
26
+ reportedFixedIds?: string[];
27
+ }
28
+ export interface ReportPartition {
29
+ newIssues: Cluster[];
30
+ continuingOpen: Cluster[];
31
+ newInvestigating: Cluster[];
32
+ continuingInvestigating: Cluster[];
33
+ fixedNew: Cluster[];
34
+ /** True when there is nothing new to report (report should be skipped to avoid noise). */
35
+ shouldSkip: boolean;
36
+ }
37
+ /**
38
+ * Partition clusters for an operator report. `now` (ISO) is used only for the
39
+ * first-run window (no prior report → items fixed in the last 4h).
40
+ */
41
+ export declare function partitionClustersForReport(clusters: Cluster[], prev: PreviousReportState, now: string): ReportPartition;
42
+ //# sourceMappingURL=reportPartition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reportPartition.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/processor/reportPartition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,mBAAmB;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,wBAAwB,CAAC,EAAE,MAAM,EAAE,CAAC;IACpC,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,OAAO,EAAE,CAAC;IACrB,cAAc,EAAE,OAAO,EAAE,CAAC;IAC1B,gBAAgB,EAAE,OAAO,EAAE,CAAC;IAC5B,uBAAuB,EAAE,OAAO,EAAE,CAAC;IACnC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,0FAA0F;IAC1F,UAAU,EAAE,OAAO,CAAC;CACrB;AAUD;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,OAAO,EAAE,EACnB,IAAI,EAAE,mBAAmB,EACzB,GAAG,EAAE,MAAM,GACV,eAAe,CA6BjB"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * reportPartition.ts — TS port of scar (d): lifecycle partitioning + re-report
3
+ * (cycling) prevention for the operator digest.
4
+ *
5
+ * Ports the pure partition decision from the report-generation function (:2747) of
6
+ * the reference `the-portal/.claude/scripts/feedback-processor.py` — the half of
7
+ * scar (d) that complements the cluster-level cycling detection already ported in
8
+ * transitions.ts. Given the current clusters + what was surfaced in the LAST
9
+ * operator report, it decides what is actionable now: newly-open vs already-known
10
+ * issues, the same for investigating, and items FIXED since the last report that
11
+ * haven't been announced before (the re-report/cycling guard — a fix is announced
12
+ * once, never again). It also decides whether the whole report should be skipped
13
+ * (nothing new ⇒ no noise).
14
+ *
15
+ * Pure: the clusters, the previous-report state, and `now` are injected (the real
16
+ * reporter does the DB query + state load + Telegram render around this). `now` is
17
+ * an ISO string. Equivalence is by faithful transcription + both-sides-of-boundary
18
+ * unit tests (the decision is embedded in a Telegram-rendering function in the
19
+ * reference, so there is no clean isolated function for a cross-runtime harness).
20
+ */
21
+ const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
22
+ function bySeverity(a, b) {
23
+ const sa = SEVERITY_ORDER[a.severity ?? ''] ?? 9;
24
+ const sb = SEVERITY_ORDER[b.severity ?? ''] ?? 9;
25
+ return sa - sb;
26
+ }
27
+ /**
28
+ * Partition clusters for an operator report. `now` (ISO) is used only for the
29
+ * first-run window (no prior report → items fixed in the last 4h).
30
+ */
31
+ export function partitionClustersForReport(clusters, prev, now) {
32
+ const prevOpen = new Set(prev.reportedOpenIds ?? []);
33
+ const prevInvestigating = new Set(prev.reportedInvestigatingIds ?? []);
34
+ const prevFixed = new Set(prev.reportedFixedIds ?? []);
35
+ const lastReportTime = prev.lastReportAt ?? '';
36
+ const allOpen = clusters.filter((c) => c.status === 'open');
37
+ const allInvestigating = clusters.filter((c) => c.status === 'investigating');
38
+ const newIssues = allOpen.filter((c) => !prevOpen.has(c.clusterId)).sort(bySeverity);
39
+ const continuingOpen = allOpen.filter((c) => prevOpen.has(c.clusterId));
40
+ const newInvestigating = allInvestigating.filter((c) => !prevInvestigating.has(c.clusterId)).sort(bySeverity);
41
+ const continuingInvestigating = allInvestigating.filter((c) => prevInvestigating.has(c.clusterId));
42
+ // Fixed since the last report AND not announced before — the re-report guard.
43
+ let cutoff;
44
+ if (lastReportTime) {
45
+ cutoff = lastReportTime;
46
+ }
47
+ else {
48
+ // First run ever — only items fixed in the last 4 hours.
49
+ cutoff = new Date(new Date(now).getTime() - 4 * 60 * 60 * 1000).toISOString();
50
+ }
51
+ const fixedNew = clusters.filter((c) => c.status === 'fixed' && String(c.updatedAt ?? '') > cutoff && !prevFixed.has(c.clusterId));
52
+ const shouldSkip = newIssues.length === 0 && newInvestigating.length === 0 && fixedNew.length === 0;
53
+ return { newIssues, continuingOpen, newInvestigating, continuingInvestigating, fixedNew, shouldSkip };
54
+ }
55
+ //# sourceMappingURL=reportPartition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reportPartition.js","sourceRoot":"","sources":["../../../src/feedback-factory/processor/reportPartition.ts"],"names":[],"mappings":"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"}
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.20",
3
+ "version": "1.3.22",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-05-27T07:58:06.900Z",
5
- "instarVersion": "1.3.20",
4
+ "generatedAt": "2026-05-27T08:47:50.347Z",
5
+ "instarVersion": "1.3.22",
6
6
  "entryCount": 192,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -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,25 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: patch -->
4
+
5
+ ## What Changed
6
+
7
+ Tenth increment of the **Feedback Factory Migration** (Dawn → Echo; spec `docs/specs/feedback-factory-migration.md`, approved). Ports the remaining half of "scar (d)" — the operator-report **lifecycle partitioning with re-report prevention** — from the reference Python (`the-portal/.claude/scripts/feedback-processor.py`, `:2747`) into a pure function at `src/feedback-factory/processor/reportPartition.ts`.
8
+
9
+ Given the current bug-clusters and what was surfaced in the last operator report, it decides what is actionable now: which issues are newly-open versus already-known, the same for in-progress work, and which bugs were fixed since the last report — announcing each fix exactly once (the re-report guard that stops the digest from repeating itself). It also decides when there's nothing new and the report should be skipped entirely. Pure function; **not wired into any route or job yet** — no behavioral change.
10
+
11
+ This completes scar (d): the cluster-level cycling was ported earlier (in the state-machine increment); this is the digest-level partitioning + re-report prevention Dawn specifically flagged.
12
+
13
+ ## What to Tell Your User
14
+
15
+ - The last piece of the feedback "brain" Dawn flagged is now ported — the logic that decides what goes into a status report and, crucially, doesn't re-announce the same fix over and over.
16
+
17
+ ## Summary of New Capabilities
18
+
19
+ | Capability | How to Use |
20
+ |-----------|-----------|
21
+ | Report partition + re-report guard (TS port) | Internal module `src/feedback-factory/processor/reportPartition.ts` — not yet wired |
22
+
23
+ ## Evidence
24
+
25
+ - The decision is embedded in a Telegram-rendering function in the reference, so equivalence is by faithful transcription plus both-sides-of-boundary tests (6 unit tests): new vs continuing open/investigating; severity ordering; the re-report guard (a fix updated after the last report and not previously announced is included; one before the last report or already announced is excluded); the first-run 4-hour window; and skip-only-when-nothing-new.
@@ -0,0 +1,33 @@
1
+ # Side-Effects Review — scar (d) report partitioning + re-report guard (Phase 1, increment 10)
2
+
3
+ **Slug:** `feedback-factory-report-partition`
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 partition decision of scar (d) — the half Dawn flagged at `:2747` that complements the cluster-level cycling already ported in transitions.ts. The Telegram rendering + report-state persistence stay in the (later) notification layer.
8
+
9
+ ## Summary of the change
10
+
11
+ Ports the operator-report partition decision into `src/feedback-factory/processor/reportPartition.ts` (`partitionClustersForReport(clusters, prev, now)`). Given the current clusters + what was surfaced in the last report, it decides: newly-open vs already-known open issues; new vs continuing investigating; items FIXED since the last report not announced before (the **re-report / cycling guard** — a fix is announced once, never again); severity ordering of the new buckets; and `shouldSkip` (nothing new ⇒ no noisy report). Pure — clusters, previous-report state, and `now` are injected. **Not wired into any route/job yet** — no behavioral change.
12
+
13
+ This completes scar (d): increment 3 ported the cluster-level cycling (`detect_cycling` + the chronic circuit-breaker); this ports the digest-level lifecycle partitioning + re-report prevention.
14
+
15
+ ## Equivalence verification
16
+
17
+ The decision is embedded in a Telegram-rendering function in the reference, so equivalence is by faithful transcription + both-sides-of-boundary unit tests (6): new vs continuing open/investigating; severity sort (critical→low); the re-report guard (fix after last-report not-previously-announced included; before-last-report or already-announced excluded); the first-run 4-hour window; and `shouldSkip` true only when nothing is new. The set-membership, the `updatedAt > cutoff` comparison, the 4h first-run window, and the skip condition are transcribed verbatim.
18
+
19
+ ## Seven-dimension review
20
+
21
+ 1. **Over/under-reach** — Pure function, no I/O, no state, not imported by any runtime path. The state load + Telegram render are correctly excluded (notification layer).
22
+ 2. **Level-of-abstraction fit** — Processor-logic layer; produces a partition the notification layer renders. The report-state persistence (`_save_last_report_state`) + Telegram formatting attach later, around this pure decision.
23
+ 3. **Signal vs Authority** — N/A; produces buckets + a skip flag. No authority over cluster lifecycle.
24
+ 4. **Interactions** — None. New isolated module; nothing imports it yet.
25
+ 5. **Rollback cost** — Trivial: delete the module + tests.
26
+ 6. **Migration parity** — N/A. New internal library code; no agent-installed file touched.
27
+ 7. **Failure modes** — (a) Re-report guard inverted (announce a fix twice / never) → both-sides tests on prevFixed + updatedAt-vs-cutoff. (b) First-run window wrong → tested at the 4h boundary. (c) Skip-when-something-new → tested both directions. (d) Severity sort → tested.
28
+
29
+ ## Tests
30
+
31
+ - Tier-1 unit (CI): `tests/unit/feedback-factory/reportPartition.test.ts` — 6 tests.
32
+ - No cross-runtime parity harness (decision embedded in a Telegram-rendering function; equivalence by transcription + boundary tests).
33
+ - No integration/E2E this increment: the digest rendering + state persistence are the notification layer, which attaches at cutover. 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.