@dzhechkov/harness-core 0.7.5 → 0.7.6
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/.dz-manifest.json +47 -27
- package/README.md +38 -1
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +6 -1
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/provenance.d.ts +6 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/provenance.js +22 -1
- package/dist/provenance.js.map +1 -1
- package/dist/skill-drift.d.ts +8 -2
- package/dist/skill-drift.d.ts.map +1 -1
- package/dist/skill-drift.js +60 -11
- package/dist/skill-drift.js.map +1 -1
- package/dist/skill-install-roots.d.ts +100 -0
- package/dist/skill-install-roots.d.ts.map +1 -0
- package/dist/skill-install-roots.js +116 -0
- package/dist/skill-install-roots.js.map +1 -0
- package/dist/tg-post.d.ts +22 -0
- package/dist/tg-post.d.ts.map +1 -1
- package/dist/tg-post.js +42 -1
- package/dist/tg-post.js.map +1 -1
- package/package.json +4 -4
- package/sbom.json +76 -26
- package/src/feature-adr-routing.ts +6 -1
- package/src/index.ts +1 -0
- package/src/provenance.ts +23 -1
- package/src/skill-drift.ts +70 -13
- package/src/skill-install-roots.ts +119 -0
- package/src/tg-post.ts +56 -1
package/src/tg-post.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
/**
|
|
2
3
|
* `dz tg-post` — the pure half: validate an approved draft against what Telegram will accept and
|
|
3
4
|
* what the channel's own accepted design demands.
|
|
@@ -103,13 +104,41 @@ export interface TgSendDecision {
|
|
|
103
104
|
* intention; without it the refusal names the local MSK time it computed, so the operator can check
|
|
104
105
|
* the arithmetic instead of trusting it.
|
|
105
106
|
*/
|
|
107
|
+
/**
|
|
108
|
+
* The dedup key for a post: sha256 of its VISIBLE text (markup stripped). Two drafts that render
|
|
109
|
+
* identically in Telegram are the same post even if their HTML differs by a whitespace — the key
|
|
110
|
+
* must be about what the reader sees, not the bytes (G5, ADR-005).
|
|
111
|
+
*/
|
|
112
|
+
export function tgVisibleSha256(html: string): string {
|
|
113
|
+
const visible = html.replace(/<[^>]*>/g, '').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
114
|
+
return createHash('sha256').update(visible.trim(), 'utf-8').digest('hex');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** One recorded send. sha256 is the visible-text key; ts is ISO. */
|
|
118
|
+
/** One journal line. status: 'pending' written BEFORE the send (crash-window guard), 'sent' after
|
|
119
|
+
* Telegram accepts. A legacy line with no status is treated as 'sent'. */
|
|
120
|
+
export interface TgSentRecord { readonly sha256: string; readonly ts: string; readonly status?: 'pending' | 'sent' }
|
|
121
|
+
|
|
106
122
|
export function decideTgSend(input: {
|
|
107
123
|
readonly issues: readonly TgHtmlIssue[];
|
|
108
124
|
readonly provenanceOutcome: 'allowed' | 'blocked' | 'not-established' | 'skipped';
|
|
109
125
|
readonly confirmed: boolean;
|
|
110
126
|
readonly nowUtcIso: string;
|
|
111
127
|
readonly nightOverride: boolean;
|
|
128
|
+
/** G6 stop-cord: a `.dz/tg-post/HALT` file exists. Checked FIRST, before anything else. */
|
|
129
|
+
readonly halted?: boolean;
|
|
130
|
+
/** sha256 of THIS post's visible text (tgVisibleSha256) — the G5 dedup key. */
|
|
131
|
+
readonly sha256?: string;
|
|
132
|
+
/** The send journal as FACTS (the pure half never reads a file). Undefined ⇒ log unreadable. */
|
|
133
|
+
readonly sentLog?: readonly TgSentRecord[] | undefined;
|
|
134
|
+
/** G4 ceiling; default 10 (ADR-003). Sends in the trailing 24h are counted against it. */
|
|
135
|
+
readonly maxPostsPerDay?: number;
|
|
112
136
|
}): TgSendDecision {
|
|
137
|
+
// G6 (ADR-001 fail-closed-guard-order): the stop-cord is ABSOLUTE and FIRST — it halts even a
|
|
138
|
+
// perfect post, before any cheaper check, so no bug in a later gate can route around it.
|
|
139
|
+
if (input.halted === true) {
|
|
140
|
+
return { action: 'refuse', reason: 'STOP-CORD engaged (.dz/tg-post/HALT exists) — nothing is published while it is present; remove the file to resume' };
|
|
141
|
+
}
|
|
113
142
|
if (input.issues.length > 0) {
|
|
114
143
|
return { action: 'refuse', reason: `${input.issues.length} formatting issue(s) — Telegram would refuse or mangle this draft` };
|
|
115
144
|
}
|
|
@@ -133,5 +162,31 @@ export function decideTgSend(input: {
|
|
|
133
162
|
return { action: 'refuse', reason: `it is ${String(mskHour).padStart(2, '0')}:xx MSK — the channel posts nothing between 00:00 and 06:00 MSK (ADR-003). Pass --night if this is deliberate` };
|
|
134
163
|
}
|
|
135
164
|
}
|
|
136
|
-
|
|
165
|
+
// G4/G5 need the journal. An UNREADABLE journal (sentLog undefined) is fail-closed: an unreadable
|
|
166
|
+
// limit counter does not prove the limit is unreached (Step-0 recall — never invert fail-closed on
|
|
167
|
+
// an absent/degraded input).
|
|
168
|
+
if (input.sentLog === undefined) {
|
|
169
|
+
return { action: 'refuse', reason: 'the send journal could not be read — an unreadable limit/dedup counter cannot show the ceiling is unreached; refusing (fail-closed)' };
|
|
170
|
+
}
|
|
171
|
+
// G5 dedup: this exact visible-text post already went out.
|
|
172
|
+
// G5 dedup covers BOTH a completed send AND an in-flight PENDING record of the same visible text
|
|
173
|
+
// (Codex A- two-phase): a pending row means a prior attempt reached the send path — do not
|
|
174
|
+
// double-publish. A stuck pending is surfaced (the CLI warns) and cleared by removing its line.
|
|
175
|
+
if (input.sha256 !== undefined && input.sentLog.some((r) => r.sha256 === input.sha256)) {
|
|
176
|
+
const prior = input.sentLog.find((r) => r.sha256 === input.sha256);
|
|
177
|
+
return { action: 'refuse', reason: (prior && prior.status === 'pending')
|
|
178
|
+
? 'a PENDING send of this exact post exists (a prior attempt reached the send path) — refusing to double-publish; if that attempt truly failed, remove its line from .dz/tg-post/sent-log.jsonl and retry'
|
|
179
|
+
: 'this exact post (by visible text) has already been sent — refusing a duplicate' };
|
|
180
|
+
}
|
|
181
|
+
// G4 daily limit: sends in the trailing 24h against the ceiling.
|
|
182
|
+
const nowMs = Date.parse(input.nowUtcIso);
|
|
183
|
+
const cutoff = Number.isFinite(nowMs) ? nowMs - 24 * 3600_000 : -Infinity;
|
|
184
|
+
// Only ACCEPTED sends count against the daily limit — a pending row may never have landed, so it
|
|
185
|
+
// must not eat the ceiling (mirror of 'record after'): status 'pending' is excluded, 'sent'/legacy counted.
|
|
186
|
+
const inWindow = input.sentLog.filter((r) => { const t = Date.parse(r.ts); return Number.isFinite(t) && t >= cutoff && r.status !== 'pending'; }).length;
|
|
187
|
+
const ceiling = typeof input.maxPostsPerDay === 'number' && input.maxPostsPerDay > 0 ? Math.floor(input.maxPostsPerDay) : 10;
|
|
188
|
+
if (inWindow >= ceiling) {
|
|
189
|
+
return { action: 'refuse', reason: `daily limit reached: ${inWindow} post(s) sent in the last 24h, ceiling ${ceiling} (ADR-003) — the channel does not exceed its own tempo` };
|
|
190
|
+
}
|
|
191
|
+
return { action: 'send', reason: 'stop-cord clear, formatted, provenance-cleared, confirmed, inside hours, not a duplicate, under the daily limit' };
|
|
137
192
|
}
|