instar 1.3.1627 → 1.3.1628
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/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/native/InstarWedgeCanary.app/Contents/MacOS/InstarWedgeCanary +0 -0
- package/dist/native/instar-desktop-host +0 -0
- package/package.json +1 -1
- package/scripts/lint-no-direct-destructive.js +101 -12
- package/src/data/builtin-manifest.json +2 -2
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/1.3.1628.md +21 -0
- package/upgrades/side-effects/lint-destructive-fail-closed.md +72 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1628",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "b7bdec77909966bae0c48ed812dc177a28d214980fa5e6f5d415831cca3395b2",
|
|
3
3
|
"registrySha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1628"
|
|
5
5
|
}
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -21,11 +21,14 @@
|
|
|
21
21
|
* for direct destructive `git <verb>` invocations (closed verb list).
|
|
22
22
|
*
|
|
23
23
|
* Exit codes:
|
|
24
|
-
* 0 —
|
|
24
|
+
* 0 — every enumerated target file was inspected and none has a violation.
|
|
25
25
|
* 1 — at least one violation.
|
|
26
|
-
* 2 — could not inspect:
|
|
27
|
-
* problem, e.g. a missing `typescript`)
|
|
28
|
-
*
|
|
26
|
+
* 2 — could not inspect: NONE of the scanned files parsed (an environment
|
|
27
|
+
* problem, e.g. a missing `typescript`), or SOME target file could not be
|
|
28
|
+
* read or parsed (or a directory could not be listed) and no violation was
|
|
29
|
+
* found elsewhere. Each uninspected path is printed with its reason.
|
|
30
|
+
* Distinct from 1 on purpose — "I could not look" is not "I looked and
|
|
31
|
+
* found nothing".
|
|
29
32
|
*
|
|
30
33
|
* Usage:
|
|
31
34
|
* node scripts/lint-no-direct-destructive.js # full repo
|
|
@@ -237,6 +240,54 @@ function hasAllowComment(text) {
|
|
|
237
240
|
|
|
238
241
|
const violations = [];
|
|
239
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Files (or directories) this run was meant to inspect but could not: unreadable,
|
|
245
|
+
* unparseable, or an unlistable directory. Each one is a hole in the scan, and a
|
|
246
|
+
* hole must not look like a clean file (#2190 / #2189) — any entry here refuses
|
|
247
|
+
* the run with the path and the reason.
|
|
248
|
+
*/
|
|
249
|
+
const uninspected = [];
|
|
250
|
+
|
|
251
|
+
function recordUninspected(file, reason) {
|
|
252
|
+
uninspected.push({ file, reason });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Errors worth one more attempt: the file may be mid-write or the fd table momentarily full. */
|
|
256
|
+
const TRANSIENT_READ_CODES = new Set(['EAGAIN', 'EBUSY', 'EINTR', 'EMFILE', 'ENFILE']);
|
|
257
|
+
const READ_RETRY_DELAYS_MS = [50, 200];
|
|
258
|
+
|
|
259
|
+
function sleepSync(ms) {
|
|
260
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Read a file, briefly retrying transient errors. Throws the last error if every attempt fails. */
|
|
264
|
+
function readFileWithRetry(file) {
|
|
265
|
+
for (let attempt = 0; ; attempt++) {
|
|
266
|
+
try {
|
|
267
|
+
// Test hook, fail-closed only (same contract as INSTAR_LINT_FORCE_PARSE_FAILURE):
|
|
268
|
+
// it can only make a read fail, never make a file pass. `<basename>` fails
|
|
269
|
+
// every attempt; `<basename>:<n>` fails only the first n (exercises the retry).
|
|
270
|
+
const forced = process.env.INSTAR_LINT_FORCE_READ_FAILURE;
|
|
271
|
+
const [forcedName, forcedTimes] = (forced || '').split(':');
|
|
272
|
+
if (forcedName && path.basename(file) === forcedName
|
|
273
|
+
&& (forcedTimes === undefined || attempt < Number(forcedTimes))) {
|
|
274
|
+
const err = new Error(`forced read failure (INSTAR_LINT_FORCE_READ_FAILURE=${forced})`);
|
|
275
|
+
err.code = 'EBUSY';
|
|
276
|
+
throw err;
|
|
277
|
+
}
|
|
278
|
+
return fs.readFileSync(file, 'utf8');
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (attempt >= READ_RETRY_DELAYS_MS.length || !TRANSIENT_READ_CODES.has(err.code)) throw err;
|
|
281
|
+
sleepSync(READ_RETRY_DELAYS_MS[attempt]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function describeError(err) {
|
|
287
|
+
const message = String(err && err.message ? err.message : err);
|
|
288
|
+
return err && err.code && !message.startsWith(err.code) ? `${err.code}: ${message}` : message;
|
|
289
|
+
}
|
|
290
|
+
|
|
240
291
|
/**
|
|
241
292
|
* Per-callsite marker: `// safe-git-allow: incremental-migration` placed on
|
|
242
293
|
* the line immediately above (or at end of) a flagged callsite suppresses
|
|
@@ -336,6 +387,12 @@ const FS_MODULE_NAMES = new Set([
|
|
|
336
387
|
* 7. Dynamic property access used to evade the rule (`fs['rm' + 'Sync']`) — flag.
|
|
337
388
|
*/
|
|
338
389
|
function lintTsFile(file, text) {
|
|
390
|
+
// Per-file variant of the INSTAR_LINT_FORCE_PARSE_FAILURE hook (a basename
|
|
391
|
+
// instead of '1'), for the partial-failure refusal. Fail-closed only.
|
|
392
|
+
const forcedParse = process.env.INSTAR_LINT_FORCE_PARSE_FAILURE;
|
|
393
|
+
if (forcedParse && forcedParse !== '1' && path.basename(file) === forcedParse) {
|
|
394
|
+
throw new Error(`forced parse failure (INSTAR_LINT_FORCE_PARSE_FAILURE=${forcedParse})`);
|
|
395
|
+
}
|
|
339
396
|
const T = ts();
|
|
340
397
|
const sf = T.createSourceFile(file, text, T.ScriptTarget.Latest, true);
|
|
341
398
|
const ctx = { text };
|
|
@@ -728,7 +785,9 @@ function lintPackageJsonScripts(file, text) {
|
|
|
728
785
|
let pkg;
|
|
729
786
|
try {
|
|
730
787
|
pkg = JSON.parse(text);
|
|
731
|
-
} catch {
|
|
788
|
+
} catch (err) {
|
|
789
|
+
// An unparseable package.json hides every npm script from the git-verb check.
|
|
790
|
+
recordUninspected(file, `could not parse as JSON: ${describeError(err)}`);
|
|
732
791
|
return;
|
|
733
792
|
}
|
|
734
793
|
const scripts = pkg.scripts || {};
|
|
@@ -751,7 +810,10 @@ function walkDir(dir, out) {
|
|
|
751
810
|
let entries;
|
|
752
811
|
try {
|
|
753
812
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
754
|
-
} catch {
|
|
813
|
+
} catch (err) {
|
|
814
|
+
// A target dir that does not exist has nothing to inspect; one that exists
|
|
815
|
+
// but cannot be listed hides every file beneath it.
|
|
816
|
+
if (err.code !== 'ENOENT') recordUninspected(dir, `could not list directory: ${describeError(err)}`);
|
|
755
817
|
return;
|
|
756
818
|
}
|
|
757
819
|
for (const e of entries) {
|
|
@@ -809,6 +871,18 @@ function gatherAll() {
|
|
|
809
871
|
return out;
|
|
810
872
|
}
|
|
811
873
|
|
|
874
|
+
function writeUninspected() {
|
|
875
|
+
if (uninspected.length === 0) return;
|
|
876
|
+
process.stderr.write('\n');
|
|
877
|
+
process.stderr.write(
|
|
878
|
+
`[lint-no-direct-destructive] COULD NOT INSPECT ${uninspected.length} path(s) — `
|
|
879
|
+
+ 'these were NOT checked, so this run cannot report them clean:\n'
|
|
880
|
+
);
|
|
881
|
+
for (const u of uninspected) {
|
|
882
|
+
process.stderr.write(` ${path.relative(ROOT, u.file) || u.file}\n ${u.reason}\n`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
812
886
|
// ── Main ───────────────────────────────────────────────────────────
|
|
813
887
|
|
|
814
888
|
function main() {
|
|
@@ -828,8 +902,9 @@ function main() {
|
|
|
828
902
|
for (const file of files) {
|
|
829
903
|
let text;
|
|
830
904
|
try {
|
|
831
|
-
text =
|
|
832
|
-
} catch {
|
|
905
|
+
text = readFileWithRetry(file);
|
|
906
|
+
} catch (err) {
|
|
907
|
+
recordUninspected(file, `could not read: ${describeError(err)}`);
|
|
833
908
|
continue;
|
|
834
909
|
}
|
|
835
910
|
|
|
@@ -854,10 +929,12 @@ function main() {
|
|
|
854
929
|
lintTsFile(file, text);
|
|
855
930
|
astParsed += 1;
|
|
856
931
|
} catch (err) {
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
|
|
932
|
+
// A thrown parse is a file this run did not inspect (#2190). It used to be a
|
|
933
|
+
// stderr warning with exit 0, so an unchecked file looked exactly like a
|
|
934
|
+
// clean one. Syntax the parser dislikes does NOT land here —
|
|
935
|
+
// createSourceFile recovers and returns a tree — so this cannot fail a
|
|
936
|
+
// build over one odd-but-valid file; it fires when inspection itself broke.
|
|
937
|
+
recordUninspected(file, `failed to parse: ${describeError(err)}`);
|
|
861
938
|
}
|
|
862
939
|
}
|
|
863
940
|
|
|
@@ -870,6 +947,7 @@ function main() {
|
|
|
870
947
|
// This is deliberately the TOTAL-failure case only, so it cannot fail a build
|
|
871
948
|
// over one file the parser dislikes: in any working checkout, files parse.
|
|
872
949
|
if (astAttempted > 0 && astParsed === 0) {
|
|
950
|
+
writeUninspected();
|
|
873
951
|
process.stderr.write('\n');
|
|
874
952
|
process.stderr.write(
|
|
875
953
|
`[lint-no-direct-destructive] COULD NOT INSPECT — ${astAttempted} file(s) were scanned `
|
|
@@ -886,6 +964,17 @@ function main() {
|
|
|
886
964
|
return 2;
|
|
887
965
|
}
|
|
888
966
|
|
|
967
|
+
// Partial holes (#2190): some files could not be inspected. Name each one and
|
|
968
|
+
// its reason. Exit 2 ("could not inspect") unless violations were also found,
|
|
969
|
+
// which stay exit 1 — a definite finding outranks an incomplete scan.
|
|
970
|
+
if (uninspected.length > 0) {
|
|
971
|
+
writeUninspected();
|
|
972
|
+
if (violations.length === 0) {
|
|
973
|
+
process.stderr.write('Fix the cause (permissions, encoding, a broken install) and re-run.\n');
|
|
974
|
+
return 2;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
889
978
|
if (violations.length === 0) {
|
|
890
979
|
return 0;
|
|
891
980
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-11-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-11-03T18:02:09.861Z",
|
|
5
|
+
"instarVersion": "1.3.1628",
|
|
6
6
|
"entryCount": 206,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1628",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "b7bdec77909966bae0c48ed812dc177a28d214980fa5e6f5d415831cca3395b2",
|
|
3
3
|
"registrySha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1628"
|
|
5
5
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The direct destructive-operation lint now reports files and directories it could not inspect. Temporary read errors are retried briefly; an unreadable or unparseable path produces a nonzero result instead of being silently skipped.
|
|
9
|
+
|
|
10
|
+
## What to Tell Your User
|
|
11
|
+
|
|
12
|
+
The source safety check now tells you when it could not inspect an enumerated source file. A clean result means it read the paths it selected to inspect.
|
|
13
|
+
|
|
14
|
+
## Summary of New Capabilities
|
|
15
|
+
|
|
16
|
+
- Names unreadable or unparseable selected files and unlistable existing directories, with the reason inspection failed.
|
|
17
|
+
- Retries short-lived file read errors before reporting that the scan was incomplete.
|
|
18
|
+
|
|
19
|
+
## Evidence
|
|
20
|
+
|
|
21
|
+
The focused lint tests cover read and parse failures, transient retries, unreadable directories, and a clean tree. The exact PR head also passed the local unit, integration, and end-to-end verification before this release-note-only follow-up; the new head still requires its own verification.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Side-Effects Review — destructive-operation lint scan completeness
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `lint-destructive-fail-closed`
|
|
4
|
+
**Date:** 2026-11-03
|
|
5
|
+
**Author:** Echo desk, integrating Justin's 2.0 agent PR #2641
|
|
6
|
+
**Second-pass reviewer:** independent reviewer subagent, 2026-11-03
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`scripts/lint-no-direct-destructive.js` now records source paths it could not read, parse, or list, and returns exit 2 when inspection was incomplete and no definite violation was found. It retries transient read failures twice. Definite violations retain exit 1 and the incomplete paths are still reported. `tests/unit/lint-no-direct-destructive.test.ts` covers these boundaries. The release fragment and ELI16 overview explain the change. The decision point is whether a lint scan may certify a target set as clean; the existing policy still decides which destructive operations violate the rule.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- Lint scan completion and exit code — **modified**: an uninspected requested path makes a clean verdict unavailable.
|
|
15
|
+
- Destructive-operation detection — **passed through**: existing patterns and allow comments are unchanged.
|
|
16
|
+
- Release and pre-commit callers — **passed through**: they consume the command's result; this patch does not change those callers.
|
|
17
|
+
|
|
18
|
+
## 1. Over-block
|
|
19
|
+
|
|
20
|
+
A legitimate source tree can now fail lint when a file is temporarily unreadable or the parser throws. Two short retries cover transient read errors, but a longer file replacement may still require a rerun. A directory that exists but cannot be listed also fails, even if it ultimately contains no relevant source file, because the scanner cannot establish that fact. A missing optional directory remains acceptable. The cost is a failed development gate with a named path, which is repairable without changing product state.
|
|
21
|
+
|
|
22
|
+
## 2. Under-block
|
|
23
|
+
|
|
24
|
+
The scanner still only inspects paths selected by its existing traversal and file-extension rules. A destructive call outside those targets or hidden behind dynamic code can evade the preexisting detector. Explicit paths that do not exist, and staged paths whose files cannot be stat'ed, are filtered before the read loop; those remain a separate missing-target gap. A target file that is modified after it was read can change before a later build step. The normal `npm run lint` chain stops on exit 2, but `scripts/pre-push-gate.js` currently turns exit 2 into a warning. That caller can still let an incomplete pre-push scan through. This patch changes the scanner's verdict for selected paths; it does not make every caller reject that verdict or claim a transactionally frozen repository snapshot.
|
|
25
|
+
|
|
26
|
+
## 3. Level-of-abstraction fit
|
|
27
|
+
|
|
28
|
+
Scan completeness belongs inside the scanner, where read, parse, and directory-list errors are visible. The existing development and release gates consume its exit code; a separate downstream checker could not recover which path was skipped. This is a structural validator for an attempted inspection, while the existing destructive-operation detector retains its policy.
|
|
29
|
+
|
|
30
|
+
## 4. Signal vs authority compliance
|
|
31
|
+
|
|
32
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
33
|
+
|
|
34
|
+
- [x] This is a hard-invariant validation: a scan cannot certify a selected path it did not inspect.
|
|
35
|
+
- [ ] This adds a semantic judgment based on a brittle signal.
|
|
36
|
+
|
|
37
|
+
The principle explicitly permits structural validation and safety guards for destructive actions. The rule here is enumerable: an attempted target path was inspected or it was not. Exit 2 says “could not inspect” and prints evidence; it does not claim a destructive operation was found or infer author intent. The existing gate owns whether to block the commit on that result. A failure may need a retry, but a clean verdict must not be fabricated.
|
|
38
|
+
|
|
39
|
+
## 4b. Judgment-point check
|
|
40
|
+
|
|
41
|
+
No new static heuristic at a competing-signals decision point. The only new choice is whether every selected path was inspected. That is an invariant of the scan, not a ranking among liveness, urgency, ownership, or conversational signals.
|
|
42
|
+
|
|
43
|
+
## 5. Interactions
|
|
44
|
+
|
|
45
|
+
- **Shadowing:** The existing total-parser-failure branch still runs first; the new partial-hole branch follows it. A definite destructive violation still exits 1. The package-script JSON parser records malformed input rather than silently skipping it.
|
|
46
|
+
- **Double-fire:** A read failure records one path once; no separate worker is started. The diagnostic lists incomplete paths when a definite violation is present, avoiding a silent second failure.
|
|
47
|
+
- **Races:** A concurrent write may produce a transient read error. Two short retries precede exit 2; rerunning remains available. There is no shared mutable state across lint processes.
|
|
48
|
+
- **Feedback loops:** The lint is invoked by development gates and may stop a commit or push, but it does not mutate the source being inspected or schedule a new run.
|
|
49
|
+
|
|
50
|
+
## 6. External surfaces
|
|
51
|
+
|
|
52
|
+
Other contributors and agents see path-specific stderr and exit 2 for an incomplete scan that previously looked clean. Existing scripts that distinguish exit 1 from exit 2 retain that distinction, including the pre-push gate's warning behavior. No Telegram, GitHub API, database, or user message is emitted by the lint. No operator-facing action is added; resolving a named unreadable file remains a developer task. Runtime product behavior is unchanged until a developer invokes the lint gate.
|
|
53
|
+
|
|
54
|
+
## 6b. Operator-surface quality
|
|
55
|
+
|
|
56
|
+
No operator surface — not applicable. No dashboard, approval, or grant form is changed.
|
|
57
|
+
|
|
58
|
+
## 7. Multi-machine posture
|
|
59
|
+
|
|
60
|
+
**Machine-local BY DESIGN.** Each checkout's files, permissions, and parser installation are machine-specific facts; each machine runs its own lint against its own source tree. The result has no agent conversation, durable user state, notice, or URL to replicate. A PR merges only after its relevant gate evidence is evaluated for the code being shipped.
|
|
61
|
+
|
|
62
|
+
## 8. Rollback cost
|
|
63
|
+
|
|
64
|
+
Revert the source and test commit, then rerun lint. No state migration or user repair is needed. A revert restores the old incomplete-scan behavior, so it should be used only if the new diagnostic itself is faulty; the named path gives a narrower repair route.
|
|
65
|
+
|
|
66
|
+
## Conclusion and class closure
|
|
67
|
+
|
|
68
|
+
The change handles unreadable files, thrown parses, malformed package scripts, and unlistable existing directories with one incomplete-scan record path. A selected file that reaches the read loop is no longer silently skipped. Remaining limits include missing paths filtered before that loop, the pre-push caller's warning treatment of exit 2, and the existing pattern policy. The focused tests and exact-head local gate are the verification surfaces; the new commit needs its own exact-head gate before merge.
|
|
69
|
+
|
|
70
|
+
## Independent second pass
|
|
71
|
+
|
|
72
|
+
Confirmed the pre-push warning and pre-enumeration missing-path gaps are disclosed under Under-block; the selected-path scan-completeness change is reversible and consistent with the structural safety-guard exception in `docs/signal-vs-authority.md`. **Concur with the review.**
|