@mjasnikovs/pi-task 0.40.2 → 0.40.3

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.
@@ -37,16 +37,25 @@ export const README_SPLIT_RE = /^#{1,2} /m;
37
37
  export function splitAtMatches(text, re) {
38
38
  const parts = [];
39
39
  let lastIndex = 0;
40
+ let acceptedEnd = 0;
40
41
  let m;
41
42
  while ((m = re.exec(text))) {
43
+ // Scan resumes ONE character in, not past the match: the regex's trailing
44
+ // `\s+` can span a newline, so a genuinely separate line-anchored
45
+ // declaration may start inside what this match consumed.
46
+ re.lastIndex = m.index + 1;
47
+ // But a match that starts inside the last ACCEPTED one is that match's own
48
+ // tail, not a new declaration, and cutting there severs a declaration from
49
+ // the modifiers and attributes that open it. `export\nfunction a(){}` is
50
+ // one declaration; so is a cargo `#[cfg(…)]` above its `impl`, which
51
+ // CARGO_DECL_SPLIT_RE absorbs on purpose and this used to hand back as a
52
+ // chunk holding nothing but the attribute.
53
+ if (m.index < acceptedEnd)
54
+ continue;
42
55
  if (m.index > lastIndex)
43
56
  parts.push(text.slice(lastIndex, m.index));
44
57
  lastIndex = m.index;
45
- // Advance by ONE, not by the match length. The regex's trailing `\s+` can
46
- // span a newline, so the next line-anchored declaration may start INSIDE
47
- // what this match consumed: `export\nfunction a(){}` is two chunks here
48
- // and one if the scan resumes past the match.
49
- re.lastIndex = m.index + 1;
58
+ acceptedEnd = m.index + m[0].length;
50
59
  }
51
60
  if (lastIndex < text.length)
52
61
  parts.push(text.slice(lastIndex));
@@ -7,4 +7,32 @@ export interface IndexResult {
7
7
  chunksWritten: number;
8
8
  contentHash: string;
9
9
  }
10
+ /**
11
+ * The gate that decides whether a package needs re-indexing.
12
+ *
13
+ * The entry file goes in SURFACED, not raw. What is cached is the extractor's
14
+ * OUTPUT, so a build whose extractor changed has stale chunks even though every
15
+ * byte on disk is identical — a crate indexed before the braced-`use` fix keeps
16
+ * `pub use crate::runtime::;` forever, because name, version and file bytes all
17
+ * still match. Surfacing here costs one file and makes the hash answer the
18
+ * question actually being asked: would re-reading produce the same chunks?
19
+ *
20
+ * The CHUNKER counts too, for the same reason: the rows are chunks, not surface,
21
+ * so a fix to where a declaration is cut leaves stale rows behind on its own. So
22
+ * does WHICH FILES are read: dropping a package's duplicate `.d.cts` twins
23
+ * changes the rows without changing a byte on disk.
24
+ *
25
+ * It is not total. An extractor change that alters only files BELOW the entry
26
+ * goes unnoticed; deleting the cache is still the escape hatch for that.
27
+ */
28
+ /**
29
+ * The chunker's own source, so a cut-point fix invalidates every cached package.
30
+ *
31
+ * `declSplitRe.source` alone does not do it: the attribute-orphaning bug was in
32
+ * `splitAtMatches`, not in any profile's regex, so the fingerprint sat still while
33
+ * the rows it describes changed. A package indexed before that fix keeps its
34
+ * dangling `#[cfg(...)]` chunks forever, and the hash is the only thing that would
35
+ * have said so.
36
+ */
37
+ export declare function chunkerFingerprint(): string;
10
38
  export declare function ensureIndexed(cache: CacheHandle, pkg: ResolvedPackage, profile?: EcosystemProfile): IndexResult;
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import {} from './docs-resolve.js';
5
- import { chunkDeclarations, chunkReadme } from './docs-chunk.js';
5
+ import { chunkDeclarations, chunkReadme, splitAtMatches } from './docs-chunk.js';
6
6
  import { ECOSYSTEMS } from './docs-ecosystems.js';
7
7
  const ZERO_SEP = Buffer.from([0]);
8
8
  /**
@@ -23,12 +23,26 @@ const ZERO_SEP = Buffer.from([0]);
23
23
  * It is not total. An extractor change that alters only files BELOW the entry
24
24
  * goes unnoticed; deleting the cache is still the escape hatch for that.
25
25
  */
26
+ /**
27
+ * The chunker's own source, so a cut-point fix invalidates every cached package.
28
+ *
29
+ * `declSplitRe.source` alone does not do it: the attribute-orphaning bug was in
30
+ * `splitAtMatches`, not in any profile's regex, so the fingerprint sat still while
31
+ * the rows it describes changed. A package indexed before that fix keeps its
32
+ * dangling `#[cfg(...)]` chunks forever, and the hash is the only thing that would
33
+ * have said so.
34
+ */
35
+ export function chunkerFingerprint() {
36
+ return `${String(splitAtMatches)}\u0000${String(chunkDeclarations)}\u0000${String(chunkReadme)}`;
37
+ }
26
38
  function computeContentHash(pkg, profile) {
27
39
  const hash = createHash('sha256');
28
40
  hash.update(Buffer.from(`${pkg.name}@${pkg.version}`, 'utf8'));
29
41
  hash.update(ZERO_SEP);
30
42
  hash.update(Buffer.from(`${profile.declSplitRe.source}\u0000${profile.commentPrefix}`, 'utf8'));
31
43
  hash.update(ZERO_SEP);
44
+ hash.update(Buffer.from(chunkerFingerprint(), 'utf8'));
45
+ hash.update(ZERO_SEP);
32
46
  // Source text, the same trick as `declSplitRe.source`: the fingerprint moves
33
47
  // whenever the selection rule does, with nothing to remember to bump.
34
48
  hash.update(Buffer.from(`${String(profile.isSurfaceFile)}\u0000${String(dropParallelDeclarations)}`
@@ -194,7 +194,7 @@ export function registerPiWorkerDocs(pi, internals = {}) {
194
194
  const r = await lookup(projectCorpus(projectName), chunks);
195
195
  if (r.kind === 'failed')
196
196
  return docsFailureResult(r.extraction, baseDetails, '');
197
- const { extraction, excerptVerified: verified, body: text } = r;
197
+ const { extraction, excerptVerified: verified, body: text, content: retrieved } = r;
198
198
  // SAME instrumentation channel as the package path below. Both branches
199
199
  // record, or "the last docs answer before the worker stopped" is
200
200
  // unanswerable whenever the last answer came from the branch that does
@@ -213,7 +213,8 @@ export function registerPiWorkerDocs(pi, internals = {}) {
213
213
  reason: 'project-source lookup — the type-only detector is not applied here',
214
214
  excerptVerified: verified,
215
215
  excerptCheck: extraction.excerptCheck,
216
- toolText: text
216
+ toolText: text,
217
+ retrievedText: retrieved
217
218
  });
218
219
  return workerAnswer(text, {
219
220
  ...baseDetails,
@@ -346,7 +347,8 @@ export function registerPiWorkerDocs(pi, internals = {}) {
346
347
  reason: typeOnly.reason,
347
348
  excerptVerified: verified,
348
349
  excerptCheck: extraction.excerptCheck,
349
- toolText: text
350
+ toolText: text,
351
+ retrievedText: concatenated
350
352
  });
351
353
  return workerAnswer(text, {
352
354
  ...baseDetails,
@@ -31,6 +31,19 @@ export interface TypeOnlyLogRecord {
31
31
  * Optional — older records still parse.
32
32
  */
33
33
  excerptCheck?: ExcerptVerification;
34
+ /**
35
+ * The retrieved chunk text handed to the extraction child, before it wrote a word.
36
+ *
37
+ * `toolText` cannot stand in for it. The tool return embeds the child's own prose,
38
+ * so scoring that prose against it asks whether the answer contains itself — two
39
+ * full runs read 13/13, 12/12, 10/10, 13/13, 4/4 and 2/2 clean, 54 answers and not
40
+ * one miss, while one of them shipped `decodeFile`, which aeson 2 does not have.
41
+ * Removing the prose leaves only the child's own CITED excerpt, a line or two, and
42
+ * flags `from_str` and `Context` as invented. Neither corpus is the one the question
43
+ * needs; this is. Optional — records written before it still parse, and a scorer
44
+ * must say "not computable" rather than guess when it is absent.
45
+ */
46
+ retrievedText?: string;
34
47
  /**
35
48
  * The tool's ENTIRE return text — version banner, npm header, the answer prose, the cited
36
49
  * excerpt, and (when it fires) the type-only banner. Optional so logs written before this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.40.2",
3
+ "version": "0.40.3",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",