instar 1.3.475 → 1.3.477
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/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +13 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/StageTransitionValidator.d.ts +15 -0
- package/dist/core/StageTransitionValidator.d.ts.map +1 -1
- package/dist/core/StageTransitionValidator.js +23 -2
- package/dist/core/StageTransitionValidator.js.map +1 -1
- package/dist/core/types.d.ts +10 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/package.json +1 -1
- package/scripts/instar-dev-precommit.js +69 -5
- package/scripts/lib/convergence-recognition.mjs +126 -0
- package/scripts/lib/throwaway-identity.mjs +150 -0
- package/scripts/throwaway-identity.mjs +77 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.476.md +33 -0
- package/upgrades/1.3.477.md +63 -0
- package/upgrades/side-effects/converging-audit-default.md +82 -0
- package/upgrades/side-effects/throwaway-identity-helper.md +48 -0
package/dist/core/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAq3CH;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA8C;IAC3E,cAAc,EAAE,UAAU;IAC1B,gBAAgB,EAAE,eAAe;IACjC,mBAAmB,EAAE,UAAU;IAC/B,aAAa,EAAE,eAAe;IAC9B,aAAa,EAAE,eAAe;IAC9B,aAAa,EAAE,eAAe;CAC/B,CAAC;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAq3CH;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA8C;IAC3E,cAAc,EAAE,UAAU;IAC1B,gBAAgB,EAAE,eAAe;IACjC,mBAAmB,EAAE,UAAU;IAC/B,aAAa,EAAE,eAAe;IAC9B,aAAa,EAAE,eAAe;IAC9B,aAAa,EAAE,eAAe;CAC/B,CAAC;AA6xGF,gFAAgF;AAChF,MAAM,CAAC,MAAM,+BAA+B,GAAG,EAAE,CAAC;AAClD,mDAAmD;AACnD,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,CAAC;AAC/C,sCAAsC;AACtC,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,kEAAkE;AAClE,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -31,6 +31,15 @@ import { fileURLToPath } from 'node:url';
|
|
|
31
31
|
import { checkEli16Overview, MIN_ELI16_CHARS } from './eli16-overview-check.mjs';
|
|
32
32
|
import { verifyProposalDerivedRunbooks } from '../skills/instar-dev/scripts/verify-proposal-derived-runbook.mjs';
|
|
33
33
|
import { classifyTier, decideRequirementSet } from './lib/classify-tier.mjs';
|
|
34
|
+
import { recognizeConvergence } from './lib/convergence-recognition.mjs';
|
|
35
|
+
|
|
36
|
+
// Report-Backed Converging Audit (docs/specs/CONVERGING-AUDIT-DEFAULT.md, Part B).
|
|
37
|
+
// The precommit reads NO config file and runs pre-compile, so it cannot import
|
|
38
|
+
// the TS config loader. The flag specReview.requireConvergenceReport is threaded
|
|
39
|
+
// in as an ENV VAR by the .husky/pre-commit hook, mirroring the existing in-file
|
|
40
|
+
// INSTAR_DEV_ALLOW_ORPHAN_DEFERRALS pattern. When unset, the new report-backing
|
|
41
|
+
// branch never runs → byte-identical to today's precommit behavior.
|
|
42
|
+
const REQUIRE_CONVERGENCE_REPORT = process.env.INSTAR_DEV_REQUIRE_CONVERGENCE_REPORT === '1';
|
|
34
43
|
|
|
35
44
|
const __filename = fileURLToPath(import.meta.url);
|
|
36
45
|
const __dirname = path.dirname(__filename);
|
|
@@ -521,10 +530,34 @@ if (!specFmMatch) {
|
|
|
521
530
|
);
|
|
522
531
|
}
|
|
523
532
|
const specFm = specFmMatch[1];
|
|
524
|
-
const convergenceMatch = specFm.match(/^\s*review-convergence\s*:\s*["']?([^"'\n]+)/m);
|
|
525
|
-
const approvedMatch = specFm.match(/^\s*approved\s*:\s*(true|"true"|'true')/m);
|
|
526
533
|
|
|
527
|
-
|
|
534
|
+
// Convergence / approval / report recognition is factored into a pure,
|
|
535
|
+
// dependency-free module (scripts/lib/convergence-recognition.mjs) that a unit
|
|
536
|
+
// test cross-checks against the TS validator's `isConvergenceTagPresent` —
|
|
537
|
+
// keeping the two gates in agreement (CONVERGING-AUDIT-DEFAULT.md, Part C). The
|
|
538
|
+
// report-existence input depends on the spec's slug + the env flag, computed
|
|
539
|
+
// just below; the recognizer does no I/O of its own.
|
|
540
|
+
|
|
541
|
+
// Slug → report path, matching StageTransitionValidator's derivation exactly.
|
|
542
|
+
const specSlugMatch = specFm.match(/^\s*slug\s*:\s*["']?([a-z0-9][a-z0-9-]{0,63})["']?\s*$/m);
|
|
543
|
+
const specSlug = specSlugMatch ? specSlugMatch[1] : '';
|
|
544
|
+
const convergenceReportRel = specSlug
|
|
545
|
+
? path.join('docs/specs/reports', `${specSlug}-convergence.md`)
|
|
546
|
+
: '';
|
|
547
|
+
// Only probe the filesystem when the report requirement is actually on. With
|
|
548
|
+
// the flag unset, reportExists stays false but is never consulted (the
|
|
549
|
+
// recognizer's reportBacked is vacuously true), so this branch is byte-inert.
|
|
550
|
+
const convergenceReportExists =
|
|
551
|
+
REQUIRE_CONVERGENCE_REPORT && convergenceReportRel
|
|
552
|
+
? fs.existsSync(path.resolve(ROOT, convergenceReportRel))
|
|
553
|
+
: false;
|
|
554
|
+
|
|
555
|
+
const recognition = recognizeConvergence(specFm, {
|
|
556
|
+
requireReport: REQUIRE_CONVERGENCE_REPORT,
|
|
557
|
+
reportExists: convergenceReportExists,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
if (!recognition.converged) {
|
|
528
561
|
blockCommit(
|
|
529
562
|
inScopeFiles,
|
|
530
563
|
[
|
|
@@ -534,7 +567,7 @@ if (!convergenceMatch) {
|
|
|
534
567
|
);
|
|
535
568
|
}
|
|
536
569
|
|
|
537
|
-
if (!
|
|
570
|
+
if (!recognition.approved) {
|
|
538
571
|
blockCommit(
|
|
539
572
|
inScopeFiles,
|
|
540
573
|
[
|
|
@@ -545,6 +578,35 @@ if (!approvedMatch) {
|
|
|
545
578
|
);
|
|
546
579
|
}
|
|
547
580
|
|
|
581
|
+
// ── Report-backing (Part B — dark, env-gated) ──
|
|
582
|
+
// When INSTAR_DEV_REQUIRE_CONVERGENCE_REPORT=1, a converged + approved spec must
|
|
583
|
+
// ALSO have its converging-audit report on disk (proving the audit RAN, not just
|
|
584
|
+
// that a tag was added). This brings the precommit UP to the formal validator's
|
|
585
|
+
// strictness (which requires the report unconditionally). With the env unset,
|
|
586
|
+
// recognition.reportBacked is always true and this branch never blocks.
|
|
587
|
+
if (!recognition.reportBacked) {
|
|
588
|
+
blockCommit(
|
|
589
|
+
inScopeFiles,
|
|
590
|
+
[
|
|
591
|
+
`Spec ${spec} is tagged review-convergence + approved, but its converging-audit`,
|
|
592
|
+
`report is missing: ${convergenceReportRel || '(spec has no valid slug to locate a report)'}`,
|
|
593
|
+
'',
|
|
594
|
+
'INSTAR_DEV_REQUIRE_CONVERGENCE_REPORT is on (specReview.requireConvergenceReport:',
|
|
595
|
+
'true) — a convergence tag without its report can fake convergence. The report is',
|
|
596
|
+
'the audit\'s proof-of-work; run /spec-converge to produce it (Phase 5 writes',
|
|
597
|
+
`docs/specs/reports/<slug>-convergence.md), then commit. To turn this requirement`,
|
|
598
|
+
'off, set specReview.requireConvergenceReport: false in your instar config.',
|
|
599
|
+
].join('\n'),
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ── Part D: surface cross-model-review depth (observe-only, never blocks) ──
|
|
604
|
+
// The converging audit records how much external (cross-model) review actually
|
|
605
|
+
// ran. Surfacing it here makes the audit's depth visible to the operator/agent
|
|
606
|
+
// reading the gate output, without ever gating on it (Signal vs. Authority).
|
|
607
|
+
const crossModelMatch = specFm.match(/^\s*cross-model-review\s*:\s*["']?([^"'\n]+)/m);
|
|
608
|
+
const crossModelReview = crossModelMatch ? crossModelMatch[1].trim() : 'not-recorded';
|
|
609
|
+
|
|
548
610
|
// ─── Step 7: ELI16 overview verification ─────────────────────────────────
|
|
549
611
|
// Every approved spec must ship with a plain-English ELI16 overview. The
|
|
550
612
|
// overview is the entry point for any reader who has to make a real decision
|
|
@@ -824,7 +886,9 @@ if (!promotionGateResult.ok) {
|
|
|
824
886
|
assertFrameworkGenerality(inScopeFiles, validTrace.trace);
|
|
825
887
|
|
|
826
888
|
console.error(
|
|
827
|
-
`[instar-dev-precommit] OK — trace ${path.basename(validTrace.entry.file)} covers ${inScopeFiles.length} in-scope file(s), artifact ${validTrace.trace.artifactPath} verified, spec ${spec} is converged + approved
|
|
889
|
+
`[instar-dev-precommit] OK — trace ${path.basename(validTrace.entry.file)} covers ${inScopeFiles.length} in-scope file(s), artifact ${validTrace.trace.artifactPath} verified, spec ${spec} is converged + approved` +
|
|
890
|
+
`${REQUIRE_CONVERGENCE_REPORT ? ` + report-backed (${convergenceReportRel})` : ''}` +
|
|
891
|
+
` [cross-model: ${crossModelReview}], ELI16 overview ${eli16Rel} present (${eli16Result.charCount} chars), promotion-gate: ${promotionGateResult.reason}.`,
|
|
828
892
|
);
|
|
829
893
|
process.exit(0);
|
|
830
894
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* convergence-recognition.mjs — the PRECOMMIT gate's pure recognizer for a
|
|
3
|
+
* spec's convergence / approval / report-backing state.
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/specs/CONVERGING-AUDIT-DEFAULT.md (Part C — gate consistency).
|
|
6
|
+
*
|
|
7
|
+
* WHY a separate pure module: the precommit (scripts/instar-dev-precommit.js)
|
|
8
|
+
* runs PRE-COMPILE and CANNOT import the TS StageTransitionValidator across the
|
|
9
|
+
* compile boundary, so the two gates cannot share a single source of truth in
|
|
10
|
+
* code. Instead, the precommit's recognition logic is factored HERE, into a
|
|
11
|
+
* tiny dependency-free function, and a unit test
|
|
12
|
+
* (tests/unit/convergence-gate-consistency.test.ts) feeds the SAME fixture
|
|
13
|
+
* table to both this recognizer and the validator's `isConvergenceTagPresent`
|
|
14
|
+
* + report logic, asserting they agree. A drift between the two gates fails CI.
|
|
15
|
+
*
|
|
16
|
+
* This module does NO I/O: callers pass the frontmatter text and the
|
|
17
|
+
* report-existence boolean. It is the precommit-side mirror of the validator's
|
|
18
|
+
* pure predicate — same convergence-tag rule (non-empty string OR boolean
|
|
19
|
+
* true), same report-backing rule (report must exist when required).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Recognize the convergence tag in a `review-convergence` frontmatter VALUE.
|
|
24
|
+
*
|
|
25
|
+
* Mirror of StageTransitionValidator.isConvergenceTagPresent: a non-empty
|
|
26
|
+
* string (the ISO timestamp the converging-audit tooling writes) OR boolean
|
|
27
|
+
* `true` (the legacy/hand-added form) counts as present. Empty string, false,
|
|
28
|
+
* null/undefined, and any other type do NOT.
|
|
29
|
+
*
|
|
30
|
+
* @param {unknown} value
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isConvergenceTagPresent(value) {
|
|
34
|
+
if (value === true) return true;
|
|
35
|
+
if (typeof value === 'string' && value.trim().length > 0) return true;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse the raw `review-convergence` value out of a YAML frontmatter BLOCK
|
|
41
|
+
* (the text between the `---` fences, NOT including the fences). Mirrors the
|
|
42
|
+
* precommit's existing lenient regex exactly so recognition stays identical.
|
|
43
|
+
*
|
|
44
|
+
* Returns the matched string value (quotes stripped, trimmed) when the line is
|
|
45
|
+
* present, or `undefined` when the key is absent. Note: this returns a STRING
|
|
46
|
+
* for both `review-convergence: true` and `review-convergence: "<ts>"` — the
|
|
47
|
+
* precommit's regex captures the literal token; `isConvergenceTagPresent`
|
|
48
|
+
* treats any non-empty captured token as present, which matches the validator's
|
|
49
|
+
* acceptance of both the boolean-true and timestamp-string forms.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} frontmatterText
|
|
52
|
+
* @returns {string | undefined}
|
|
53
|
+
*/
|
|
54
|
+
export function parseConvergenceValue(frontmatterText) {
|
|
55
|
+
const m = String(frontmatterText).match(
|
|
56
|
+
/^\s*review-convergence\s*:\s*["']?([^"'\n]+)/m,
|
|
57
|
+
);
|
|
58
|
+
if (!m) return undefined;
|
|
59
|
+
return m[1].trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse whether the spec carries an `approved: true` tag (precommit's regex).
|
|
64
|
+
*
|
|
65
|
+
* @param {string} frontmatterText
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
export function isApprovedTagPresent(frontmatterText) {
|
|
69
|
+
return /^\s*approved\s*:\s*(true|"true"|'true')/m.test(String(frontmatterText));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The precommit's verdict for "is this spec converged + approved (+ report-backed
|
|
74
|
+
* when required)?" — the same question the formal validator answers, expressed
|
|
75
|
+
* over frontmatter text + a report-existence boolean.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} frontmatterText — the YAML frontmatter block (between the `---` fences).
|
|
78
|
+
* @param {{ requireReport?: boolean, reportExists?: boolean }} [opts]
|
|
79
|
+
* requireReport — true when INSTAR_DEV_REQUIRE_CONVERGENCE_REPORT=1.
|
|
80
|
+
* reportExists — whether docs/specs/reports/<slug>-convergence.md exists.
|
|
81
|
+
* @returns {{ converged: boolean, approved: boolean, reportBacked: boolean,
|
|
82
|
+
* accepted: boolean, reason: string }}
|
|
83
|
+
* - converged: the convergence tag is present.
|
|
84
|
+
* - approved: the approved tag is present.
|
|
85
|
+
* - reportBacked: the report requirement is satisfied (vacuously true when
|
|
86
|
+
* requireReport is false).
|
|
87
|
+
* - accepted: converged AND approved AND reportBacked — the precommit's
|
|
88
|
+
* Step-6 verdict for a tier-2/3 spec.
|
|
89
|
+
* - reason: a short machine-stable reason for the verdict.
|
|
90
|
+
*/
|
|
91
|
+
export function recognizeConvergence(frontmatterText, opts = {}) {
|
|
92
|
+
const requireReport = opts.requireReport === true;
|
|
93
|
+
const reportExists = opts.reportExists === true;
|
|
94
|
+
|
|
95
|
+
const convergenceValue = parseConvergenceValue(frontmatterText);
|
|
96
|
+
const converged = isConvergenceTagPresent(convergenceValue);
|
|
97
|
+
const approved = isApprovedTagPresent(frontmatterText);
|
|
98
|
+
const reportBacked = !requireReport || reportExists;
|
|
99
|
+
|
|
100
|
+
let reason;
|
|
101
|
+
if (!converged) reason = 'convergence-tag-missing';
|
|
102
|
+
else if (!approved) reason = 'approved-tag-missing';
|
|
103
|
+
else if (!reportBacked) reason = 'convergence-report-missing';
|
|
104
|
+
else reason = 'accepted';
|
|
105
|
+
|
|
106
|
+
const accepted = converged && approved && reportBacked;
|
|
107
|
+
return { converged, approved, reportBacked, accepted, reason };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Convenience: the precommit-side answer to "is this spec CONVERGED?" in the
|
|
112
|
+
* exact sense the consistency test compares against the validator's converged
|
|
113
|
+
* verdict (convergence tag present AND report-backed when required). Approval
|
|
114
|
+
* is a separate downstream gate in BOTH paths, so it is NOT folded in here.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} frontmatterText
|
|
117
|
+
* @param {{ requireReport?: boolean, reportExists?: boolean }} [opts]
|
|
118
|
+
* @returns {boolean}
|
|
119
|
+
*/
|
|
120
|
+
export function isSpecConverged(frontmatterText, opts = {}) {
|
|
121
|
+
const requireReport = opts.requireReport === true;
|
|
122
|
+
const reportExists = opts.reportExists === true;
|
|
123
|
+
const converged = isConvergenceTagPresent(parseConvergenceValue(frontmatterText));
|
|
124
|
+
const reportBacked = !requireReport || reportExists;
|
|
125
|
+
return converged && reportBacked;
|
|
126
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* throwaway-identity.mjs — mint genuinely-distinct, readable throwaway email
|
|
3
|
+
* identities for live-integration test harnesses (Slack, Discord, …).
|
|
4
|
+
*
|
|
5
|
+
* Backed by the mail.tm public disposable-mailbox API. Each minted inbox is a
|
|
6
|
+
* real, distinct, readable address — so a live test can use N of them as N
|
|
7
|
+
* GENUINELY DISTINCT principals (distinct addresses → distinct provider user
|
|
8
|
+
* IDs) with ZERO real accounts. This is the autonomous half of the
|
|
9
|
+
* test-identity provisioning the Live Integration Security-Test Harness needs;
|
|
10
|
+
* the only step it does NOT cover is an anti-bot signup CAPTCHA at workspace/
|
|
11
|
+
* account creation, which is a deliberate human-verification control (see
|
|
12
|
+
* docs/specs/JUDGMENT-PERMISSION-LIVE-RUN-RUNBOOK.md).
|
|
13
|
+
*
|
|
14
|
+
* Pure helpers (extractCode/extractLink/matchMessage) and HTTP helpers with an
|
|
15
|
+
* injectable `fetchImpl` so the unit test runs fully hermetic (no network).
|
|
16
|
+
*
|
|
17
|
+
* CLI: scripts/throwaway-identity.mjs (mint | wait). This file is the importable
|
|
18
|
+
* library; the CLI is a thin wrapper.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const DEFAULT_API_BASE = 'https://api.mail.tm';
|
|
22
|
+
|
|
23
|
+
/** Extract the first verification code (default: a 6-digit run) from a message body. */
|
|
24
|
+
export function extractCode(text, { pattern = /\b(\d{6})\b/ } = {}) {
|
|
25
|
+
if (typeof text !== 'string') return null;
|
|
26
|
+
const m = text.match(pattern);
|
|
27
|
+
return m ? (m[1] ?? m[0]) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Extract the first URL (optionally matching a substring/regex) from a message body. */
|
|
31
|
+
export function extractLink(text, { match } = {}) {
|
|
32
|
+
if (typeof text !== 'string') return null;
|
|
33
|
+
const urls = text.match(/https?:\/\/[^\s"'<>)\]]+/g) || [];
|
|
34
|
+
if (!match) return urls[0] ?? null;
|
|
35
|
+
const test = match instanceof RegExp ? (u) => match.test(u) : (u) => u.includes(match);
|
|
36
|
+
return urls.find(test) ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Does a mail.tm message summary match the caller's filter (subject/from substring or regex)? */
|
|
40
|
+
export function matchMessage(msg, { subject, from } = {}) {
|
|
41
|
+
if (!msg) return false;
|
|
42
|
+
const subjOk = matchField(msg.subject, subject);
|
|
43
|
+
const fromAddr = (msg.from && (msg.from.address || msg.from.name)) || '';
|
|
44
|
+
const fromOk = matchField(fromAddr, from);
|
|
45
|
+
return subjOk && fromOk;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function matchField(value, filter) {
|
|
49
|
+
if (filter === undefined || filter === null) return true;
|
|
50
|
+
const v = String(value ?? '');
|
|
51
|
+
return filter instanceof RegExp ? filter.test(v) : v.toLowerCase().includes(String(filter).toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function memberArray(json) {
|
|
55
|
+
// mail.tm is a Hydra/JSON-LD API: collections live under "hydra:member".
|
|
56
|
+
if (Array.isArray(json)) return json;
|
|
57
|
+
if (json && Array.isArray(json['hydra:member'])) return json['hydra:member'];
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function jsonFetch(fetchImpl, url, opts) {
|
|
62
|
+
const res = await fetchImpl(url, opts);
|
|
63
|
+
const text = await res.text();
|
|
64
|
+
let body = null;
|
|
65
|
+
try { body = text ? JSON.parse(text) : null; } catch { body = null; }
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const detail = (body && (body['hydra:description'] || body.message || body.detail)) || text.slice(0, 200);
|
|
68
|
+
throw new Error(`mail.tm ${opts?.method || 'GET'} ${url} → ${res.status}: ${detail}`);
|
|
69
|
+
}
|
|
70
|
+
return body;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Mint a fresh throwaway inbox: pick a domain, create the account, get a token.
|
|
75
|
+
* Returns { address, password, token, accountId }. `rand` is injectable so the
|
|
76
|
+
* test is deterministic and the script never calls Math.random in a workflow.
|
|
77
|
+
*/
|
|
78
|
+
export async function createInbox({
|
|
79
|
+
fetchImpl = fetch,
|
|
80
|
+
apiBase = DEFAULT_API_BASE,
|
|
81
|
+
localPart,
|
|
82
|
+
rand = () => Math.floor(Math.random() * 1e10).toString(36),
|
|
83
|
+
} = {}) {
|
|
84
|
+
const domains = memberArray(await jsonFetch(fetchImpl, `${apiBase}/domains`));
|
|
85
|
+
const domain = domains.find((d) => d.isActive !== false)?.domain || domains[0]?.domain;
|
|
86
|
+
if (!domain) throw new Error('mail.tm: no available domain');
|
|
87
|
+
const local = localPart || `echo-${rand()}`;
|
|
88
|
+
const address = `${local}@${domain}`;
|
|
89
|
+
const password = `Echo!${rand()}A9`;
|
|
90
|
+
const account = await jsonFetch(fetchImpl, `${apiBase}/accounts`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
body: JSON.stringify({ address, password }),
|
|
94
|
+
});
|
|
95
|
+
const tokenResp = await jsonFetch(fetchImpl, `${apiBase}/token`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/json' },
|
|
98
|
+
body: JSON.stringify({ address, password }),
|
|
99
|
+
});
|
|
100
|
+
if (!tokenResp?.token) throw new Error('mail.tm: token request returned no token');
|
|
101
|
+
return { address, password, token: tokenResp.token, accountId: account?.id ?? null };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** List message summaries in the inbox (most-recent first, per mail.tm). */
|
|
105
|
+
export async function listMessages(token, { fetchImpl = fetch, apiBase = DEFAULT_API_BASE } = {}) {
|
|
106
|
+
const body = await jsonFetch(fetchImpl, `${apiBase}/messages`, {
|
|
107
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
108
|
+
});
|
|
109
|
+
return memberArray(body);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Fetch one message's full body (text + html). */
|
|
113
|
+
export async function getMessage(token, id, { fetchImpl = fetch, apiBase = DEFAULT_API_BASE } = {}) {
|
|
114
|
+
return jsonFetch(fetchImpl, `${apiBase}/messages/${id}`, {
|
|
115
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Poll the inbox until a message matches the filter, then return its full body.
|
|
121
|
+
* `sleep` and `now` are injectable so the test runs without real timers/clock.
|
|
122
|
+
* Throws on timeout. Returns the full message (with .text / .html / .subject).
|
|
123
|
+
*/
|
|
124
|
+
export async function waitForMessage(token, {
|
|
125
|
+
subject,
|
|
126
|
+
from,
|
|
127
|
+
timeoutMs = 120_000,
|
|
128
|
+
intervalMs = 3_000,
|
|
129
|
+
fetchImpl = fetch,
|
|
130
|
+
apiBase = DEFAULT_API_BASE,
|
|
131
|
+
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
132
|
+
now = () => Date.now(),
|
|
133
|
+
} = {}) {
|
|
134
|
+
const deadline = now() + timeoutMs;
|
|
135
|
+
// First iteration runs immediately (no upfront sleep).
|
|
136
|
+
for (let first = true; ; first = false) {
|
|
137
|
+
if (!first) {
|
|
138
|
+
if (now() >= deadline) {
|
|
139
|
+
throw new Error(`waitForMessage: timed out after ${timeoutMs}ms (subject=${subject ?? '*'} from=${from ?? '*'})`);
|
|
140
|
+
}
|
|
141
|
+
await sleep(intervalMs);
|
|
142
|
+
}
|
|
143
|
+
const summaries = await listMessages(token, { fetchImpl, apiBase });
|
|
144
|
+
const hit = summaries.find((m) => matchMessage(m, { subject, from }));
|
|
145
|
+
if (hit) return getMessage(token, hit.id, { fetchImpl, apiBase });
|
|
146
|
+
if (first && now() >= deadline) {
|
|
147
|
+
throw new Error(`waitForMessage: timed out after ${timeoutMs}ms (subject=${subject ?? '*'} from=${from ?? '*'})`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* throwaway-identity.mjs — CLI for the disposable-identity helper (lib/throwaway-identity.mjs).
|
|
4
|
+
*
|
|
5
|
+
* node scripts/throwaway-identity.mjs mint
|
|
6
|
+
* → mints a fresh inbox; prints JSON { address, password, token, accountId }.
|
|
7
|
+
*
|
|
8
|
+
* node scripts/throwaway-identity.mjs wait <token> [--subject S] [--from F]
|
|
9
|
+
* [--code | --link [MATCH]] [--timeout MS]
|
|
10
|
+
* → polls the inbox until a matching message arrives, then prints either
|
|
11
|
+
* the full message JSON, or (with --code/--link) just the extracted value.
|
|
12
|
+
*
|
|
13
|
+
* Use for live-integration test-identity provisioning (the autonomous half — an
|
|
14
|
+
* anti-bot signup CAPTCHA at account creation is the human handoff).
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
createInbox,
|
|
18
|
+
waitForMessage,
|
|
19
|
+
extractCode,
|
|
20
|
+
extractLink,
|
|
21
|
+
} from './lib/throwaway-identity.mjs';
|
|
22
|
+
|
|
23
|
+
function parseFlags(args) {
|
|
24
|
+
const out = { _: [] };
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const a = args[i];
|
|
27
|
+
if (a === '--code') out.code = true;
|
|
28
|
+
else if (a === '--link') { out.link = true; if (args[i + 1] && !args[i + 1].startsWith('--')) out.linkMatch = args[++i]; }
|
|
29
|
+
else if (a === '--subject') out.subject = args[++i];
|
|
30
|
+
else if (a === '--from') out.from = args[++i];
|
|
31
|
+
else if (a === '--timeout') out.timeout = parseInt(args[++i], 10);
|
|
32
|
+
else out._.push(a);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
39
|
+
const flags = parseFlags(rest);
|
|
40
|
+
|
|
41
|
+
if (cmd === 'mint') {
|
|
42
|
+
const inbox = await createInbox();
|
|
43
|
+
process.stdout.write(JSON.stringify(inbox) + '\n');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (cmd === 'wait') {
|
|
48
|
+
const token = flags._[0];
|
|
49
|
+
if (!token) { console.error('usage: throwaway-identity.mjs wait <token> [--subject S] [--from F] [--code|--link [MATCH]] [--timeout MS]'); process.exit(1); }
|
|
50
|
+
const msg = await waitForMessage(token, {
|
|
51
|
+
subject: flags.subject,
|
|
52
|
+
from: flags.from,
|
|
53
|
+
timeoutMs: flags.timeout ?? 120_000,
|
|
54
|
+
});
|
|
55
|
+
const text = msg.text || msg.html?.join?.('\n') || (Array.isArray(msg.html) ? msg.html.join('\n') : msg.html) || '';
|
|
56
|
+
if (flags.code) {
|
|
57
|
+
const code = extractCode(text);
|
|
58
|
+
if (!code) { console.error('no verification code found in message'); process.exit(1); }
|
|
59
|
+
process.stdout.write(code + '\n');
|
|
60
|
+
} else if (flags.link) {
|
|
61
|
+
const link = extractLink(text, flags.linkMatch ? { match: flags.linkMatch } : {});
|
|
62
|
+
if (!link) { console.error('no link found in message'); process.exit(1); }
|
|
63
|
+
process.stdout.write(link + '\n');
|
|
64
|
+
} else {
|
|
65
|
+
process.stdout.write(JSON.stringify({ subject: msg.subject, from: msg.from, text }) + '\n');
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.error('usage: throwaway-identity.mjs <mint | wait>');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
import { pathToFileURL } from 'node:url';
|
|
75
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
76
|
+
main().catch((err) => { console.error(err instanceof Error ? err.message : String(err)); process.exit(1); });
|
|
77
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-10T19:33:00.272Z",
|
|
5
|
+
"instarVersion": "1.3.477",
|
|
6
6
|
"entryCount": 201,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Added a standalone test utility — `scripts/throwaway-identity.mjs` (+ `scripts/lib/`) — that
|
|
9
|
+
mints genuinely-distinct, readable throwaway email inboxes via the mail.tm public
|
|
10
|
+
disposable-mailbox API and extracts verification codes/links from them. It's the autonomous
|
|
11
|
+
half of provisioning distinct test identities for the Live Integration Security-Test Harness
|
|
12
|
+
(Slack today, any integration later): N distinct inboxes → N genuinely-distinct principals,
|
|
13
|
+
zero real accounts. No runtime code, gate, or config is touched.
|
|
14
|
+
|
|
15
|
+
## What to Tell Your User
|
|
16
|
+
|
|
17
|
+
Nothing changes in how the agent runs. This is a developer/test tool. It lets the agent set
|
|
18
|
+
up several genuinely-different throwaway test users (and read their email) on its own, so a
|
|
19
|
+
live integration test can use real distinct identities without anyone hand-creating email
|
|
20
|
+
accounts. The only step it intentionally leaves to a human is passing an anti-bot CAPTCHA at
|
|
21
|
+
workspace creation.
|
|
22
|
+
|
|
23
|
+
## Summary of New Capabilities
|
|
24
|
+
|
|
25
|
+
- `scripts/throwaway-identity.mjs mint` — create a fresh readable throwaway inbox.
|
|
26
|
+
- `scripts/throwaway-identity.mjs wait <token>` — await an email and extract its code/link.
|
|
27
|
+
- `scripts/lib/throwaway-identity.mjs` — importable helper (HTTP injectable for hermetic tests).
|
|
28
|
+
|
|
29
|
+
## Evidence
|
|
30
|
+
|
|
31
|
+
- 15 hermetic unit tests (`tests/unit/throwaway-identity.test.ts`) — pure extractors + the
|
|
32
|
+
mint / poll-until-match / timeout flow, with fetch + clock injected (no network).
|
|
33
|
+
- Live CLI smoke minted a real inbox + token. `tsc --noEmit` clean.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: minor -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Spec #4 of the cartographer-conformance project — **fixes a real defect in the
|
|
9
|
+
convergence gate and makes the report-backed converging audit the structural
|
|
10
|
+
default** (dark, off by default).
|
|
11
|
+
|
|
12
|
+
The defect: the formal initiative gate (`StageTransitionValidator`) required the
|
|
13
|
+
`review-convergence` frontmatter tag to equal the boolean `true`, but the actual
|
|
14
|
+
converging-audit tooling writes an ISO **timestamp string**. A timestamp is not the
|
|
15
|
+
boolean `true`, so the formal gate **rejected every properly-converged spec** at the
|
|
16
|
+
`spec-drafted → spec-converged` transition — quietly broken. This change adds a pure
|
|
17
|
+
`isConvergenceTagPresent` predicate that accepts the canonical timestamp (and still
|
|
18
|
+
the legacy boolean), used as the single definition of "the tag is present."
|
|
19
|
+
|
|
20
|
+
On top of the defect repair, a new default-off flag `specReview.requireConvergenceReport`
|
|
21
|
+
makes the converging audit's **report** (the proof it actually ran) a requirement of
|
|
22
|
+
the commit-time gate when enabled — so "converged" can mean "the audit ran and left a
|
|
23
|
+
report," not just "a tag was added." The commit-time gate reads the flag via an
|
|
24
|
+
environment variable (it runs before compilation and reads no config), exported from
|
|
25
|
+
config by the husky hook; when unset, the commit-time gate behaves exactly as before.
|
|
26
|
+
The formal gate's existing report requirement is left unconditional, so enabling the
|
|
27
|
+
flag brings the two gates into agreement — and a cross-gate consistency test makes it
|
|
28
|
+
impossible for them to drift apart. Whether an outside model also reviewed a spec is
|
|
29
|
+
surfaced in the gate's diagnostics, never required.
|
|
30
|
+
|
|
31
|
+
## What to Tell Your User
|
|
32
|
+
|
|
33
|
+
- **The review gate was quietly broken — now it's fixed and means something**: "The
|
|
34
|
+
check that confirms a design was properly reviewed before it ships had a bug — it was
|
|
35
|
+
looking for the wrong marker, so it actually rejected every correctly-reviewed
|
|
36
|
+
design. I repaired it, and added an opt-in setting that makes 'reviewed' require the
|
|
37
|
+
review's report to actually exist, not just a tag. It's off by default, so nothing in
|
|
38
|
+
your current workflow changes until you turn it on."
|
|
39
|
+
|
|
40
|
+
## Summary of New Capabilities
|
|
41
|
+
|
|
42
|
+
| Capability | How to Use |
|
|
43
|
+
|-----------|-----------|
|
|
44
|
+
| Report-backed convergence gate | `specReview.requireConvergenceReport: true` in config (opt-in; off by default) |
|
|
45
|
+
| Correct convergence-tag recognition | automatic — the formal gate now accepts the canonical timestamp tag |
|
|
46
|
+
|
|
47
|
+
## Evidence
|
|
48
|
+
|
|
49
|
+
- **Before:** a spec converged via the real tooling carries
|
|
50
|
+
`review-convergence: "2026-06-10T…"`. Feeding it to the formal gate returned
|
|
51
|
+
`CONVERGENCE_TAG_MISSING` because `"2026-06-10T…" !== true` — the `spec-drafted →
|
|
52
|
+
spec-converged` transition could not advance any real spec. (The existing tests only
|
|
53
|
+
ever fed the boolean `true`, which is why the defect shipped unnoticed.)
|
|
54
|
+
- **After:** the same timestamp-tagged + reported spec advances through the real
|
|
55
|
+
`POST /projects/:id/advance` route (new integration test); a timestamp-tagged spec
|
|
56
|
+
with a MISSING report still returns `CONVERGENCE_REPORT_MISSING` (the existing,
|
|
57
|
+
unchanged check); and a new predicate test covers timestamp / boolean / empty /
|
|
58
|
+
missing.
|
|
59
|
+
- **Back-compat verified end-to-end:** a test runs the real commit-time gate script —
|
|
60
|
+
with the flag unset, a timestamp-tagged + approved spec with no report commits
|
|
61
|
+
cleanly (identical to today); with the flag on and no report, it is blocked; with
|
|
62
|
+
the report present, it commits. This PR's own commit passes through the modified gate
|
|
63
|
+
with the flag off.
|