@precedence-dev/instrument 0.1.0
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/LICENSE +105 -0
- package/README.md +109 -0
- package/bin/precedence-instrument.js +12 -0
- package/dist/discover.d.ts +10 -0
- package/dist/discover.js +95 -0
- package/dist/generate/cli.d.ts +2 -0
- package/dist/generate/cli.js +368 -0
- package/dist/generate/explain.d.ts +21 -0
- package/dist/generate/explain.js +428 -0
- package/dist/generate/instrument.d.ts +165 -0
- package/dist/generate/instrument.js +665 -0
- package/dist/generate/unplugin.d.ts +31 -0
- package/dist/generate/unplugin.js +125 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.js +7 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, ALv2 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-ALv2
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Precedence
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @precedence-dev/instrument
|
|
2
|
+
|
|
3
|
+
Splices tracking calls into your source (or your build, via a bundler plugin)
|
|
4
|
+
from a **plan** — a list of exact anchors (file, structural id, condition
|
|
5
|
+
fingerprint) that [`@precedence-dev/cli`](https://github.com/precedence-dev/core)
|
|
6
|
+
already resolved. This package does not decide *what* to track or *where* — it
|
|
7
|
+
only knows how to turn an already-decided anchor into a concrete, safe code
|
|
8
|
+
edit and apply it.
|
|
9
|
+
|
|
10
|
+
FSL-1.1-ALv2 (converts to Apache-2.0 two years after each release), like the rest
|
|
11
|
+
of Precedence. It's split from the analysis engine so
|
|
12
|
+
you can audit the part that touches your source on its own — every edit
|
|
13
|
+
primitive is additive, every emitted call is fire-and-forget, and the tests
|
|
14
|
+
actually run the generated code to prove it.
|
|
15
|
+
|
|
16
|
+
## Use
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
# dry-run: print the diff, touch nothing
|
|
20
|
+
precedence-instrument --plan plan.json --dir src --track "track from @/lib/analytics"
|
|
21
|
+
|
|
22
|
+
# apply it (refuses on a dirty git tree, so the injection lands in its own commit)
|
|
23
|
+
precedence-instrument --plan plan.json --dir src --track "track from @/lib/analytics" --apply
|
|
24
|
+
|
|
25
|
+
# CI: fail the build if the committed source has drifted from the plan
|
|
26
|
+
precedence-instrument --plan plan.json --dir src --track track --check
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`--plan` is the events export from the picker (`@precedence-dev/sdk`) or the viewer
|
|
30
|
+
(`@precedence-dev/viewer`). `--dir` is repeatable. `-h` prints every flag.
|
|
31
|
+
|
|
32
|
+
## Emit modes
|
|
33
|
+
|
|
34
|
+
| `--emit` | injected call | change an event without a rebuild? |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `direct` (default) | `track("event_name", { pm_id, ...props })`, baked in; adds the `--track` import | no — it's all in the source |
|
|
37
|
+
| `runtime` | `globalThis.__pm?.("<anchor id>", { ...in-scope props })`, no import | yes — enable/disable, rename, retarget, change the discriminator, narrow props: all plan edits |
|
|
38
|
+
|
|
39
|
+
`runtime` mode needs [`@precedence-dev/sdk`](https://github.com/precedence-dev/sdk)'s
|
|
40
|
+
`installPrecedence` called once at your app root — it maps the anchor id back to
|
|
41
|
+
the event name, adds `pm_id`, layers on the plan's static props, and catches the
|
|
42
|
+
clicks on synthetic anchors (which `runtime` mode only stamps with `data-precedence-id`,
|
|
43
|
+
no listener). In `direct` mode the delegated listener is generated instead —
|
|
44
|
+
`--runtime <file>` writes it, import it once at the app root.
|
|
45
|
+
|
|
46
|
+
## Debug a live event: `--explain`
|
|
47
|
+
|
|
48
|
+
Every emitted event carries `pm_id` (the structural anchor id). Copy one from
|
|
49
|
+
the dashboard and run:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
precedence-instrument --plan plan.json --dir src --explain "src/Checkout.tsx#Checkout::form|onSubmit|result_ok"
|
|
53
|
+
precedence-instrument --plan plan.json --dir src --explain "<pm_id>" --json # structured
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
It doesn't instrument anything — it traces the id back to its exact fire site
|
|
57
|
+
and reports, per property, the **structural** reasons a value can arrive null
|
|
58
|
+
(out of scope here → baked `undefined`; the branch fires precisely when the
|
|
59
|
+
value is absent; an ambient store that can be empty; a nullable hop in the
|
|
60
|
+
accessor path; a state/data-hook binding that starts empty). It can't see
|
|
61
|
+
runtime data quality — an API that returned null shows as "resolves".
|
|
62
|
+
|
|
63
|
+
## What it actually does — and doesn't
|
|
64
|
+
|
|
65
|
+
- Every edit primitive (`block-prepend`, `wrap-expr`, `synthesize-else`,
|
|
66
|
+
`split-and`) is purely additive: it inserts new code, it never rewrites or
|
|
67
|
+
restructures anything that was already there. See `src/generate/instrument.ts`.
|
|
68
|
+
- Every emitted call is wrapped so it can never affect the surrounding control
|
|
69
|
+
flow, even if it throws or its returned promise rejects — `safeStatement`/
|
|
70
|
+
`safeExpr` in the same file. This is a provable property, not an assumption:
|
|
71
|
+
see the `fire-and-forget:` checks in `test/invariants.mjs`, which actually
|
|
72
|
+
*run* the generated code with a throwing and a rejecting `track()` and
|
|
73
|
+
assert nothing else breaks.
|
|
74
|
+
- It never invents an anchor. If a plan's anchor doesn't resolve against the
|
|
75
|
+
current source (renamed, moved, deleted), it's reported `skipped` with a
|
|
76
|
+
precise reason — never silently guessed.
|
|
77
|
+
- Two delivery paths, same resolver: `instrument()` (the CLI codemod, writes
|
|
78
|
+
to disk) and `instrumentFile()` (the bundler transform in
|
|
79
|
+
`src/generate/unplugin.ts`, source never touches disk — the tracking calls
|
|
80
|
+
exist only in the built bundle).
|
|
81
|
+
|
|
82
|
+
## Depending on `@precedence-dev/cli`
|
|
83
|
+
|
|
84
|
+
`instrument()` re-runs `@precedence-dev/cli`'s `analyze()` so it derives every id and
|
|
85
|
+
fingerprint the exact same way the scan did. `package.json` depends on
|
|
86
|
+
`@precedence-dev/cli` as a normal semver dependency.
|
|
87
|
+
|
|
88
|
+
## Structure
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
src/
|
|
92
|
+
├── generate/
|
|
93
|
+
│ ├── instrument.ts the resolver + edit primitives + safety wrapping
|
|
94
|
+
│ ├── unplugin.ts the bundler-plugin delivery path
|
|
95
|
+
│ ├── explain.ts trace a fired event's pm_id back to its source
|
|
96
|
+
│ └── cli.ts the precedence-instrument binary
|
|
97
|
+
├── discover.ts resolve --dir/--files/--changed-since to a file list
|
|
98
|
+
└── util.ts (tiny, no analysis logic — see below)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`discover.ts` and `util.ts` are intentionally duplicated from `@precedence-dev/cli`'s
|
|
102
|
+
own copies rather than imported: they're plain fs/git utilities with no
|
|
103
|
+
analysis logic in them, so keeping them local avoids a cross-package
|
|
104
|
+
dependency for plain fs/git helpers. The one thing to
|
|
105
|
+
keep in sync by hand is `--changed-since` / `--dir` semantics: run the scan and
|
|
106
|
+
the instrument step over the same file set, or an anchor the scan produced can
|
|
107
|
+
land outside what instrument looked at and read as `skipped`. `@precedence-dev/cli`'s
|
|
108
|
+
`analyze()` — the id + fingerprint derivation, which *is* shared — is bit-for-bit
|
|
109
|
+
identical between the two; only the file-list front differs.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Thin entry point. The real CLI is TypeScript in src/, compiled to dist/.
|
|
3
|
+
"use strict";
|
|
4
|
+
try {
|
|
5
|
+
require("../dist/generate/cli.js");
|
|
6
|
+
} catch (err) {
|
|
7
|
+
if (err && err.code === "MODULE_NOT_FOUND" && /dist[\\/]generate/.test(String(err.message))) {
|
|
8
|
+
process.stderr.write("@precedence-dev/instrument is not built yet - run `npm run build` first.\n");
|
|
9
|
+
process.exit(2);
|
|
10
|
+
}
|
|
11
|
+
throw err;
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface DiscoverInput {
|
|
2
|
+
paths?: string[];
|
|
3
|
+
files?: string[];
|
|
4
|
+
changedSince?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DiscoverResult {
|
|
7
|
+
files: string[];
|
|
8
|
+
errors: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function discoverSources(input: DiscoverInput): DiscoverResult;
|
package/dist/discover.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.discoverSources = discoverSources;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve CLI-style inputs (dirs / files / a git ref) to a sorted file list.
|
|
39
|
+
*
|
|
40
|
+
* Duplicated from @precedence-dev/cli's own copy rather than imported from it: this
|
|
41
|
+
* is a plain fs/git utility with no analysis logic in it, so keeping it local
|
|
42
|
+
* keeps this package importable without pulling in the whole engine.
|
|
43
|
+
*/
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const child_process_1 = require("child_process");
|
|
47
|
+
const util_1 = require("./util");
|
|
48
|
+
const SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".cts", ".mts", ".cjs", ".mjs"];
|
|
49
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage"]);
|
|
50
|
+
const isSource = (f) => SOURCE_EXTENSIONS.includes(path.extname(f).toLowerCase());
|
|
51
|
+
function walkDir(dir, out) {
|
|
52
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
53
|
+
if (e.isDirectory()) {
|
|
54
|
+
if (!SKIP_DIRS.has(e.name))
|
|
55
|
+
walkDir(path.join(dir, e.name), out);
|
|
56
|
+
}
|
|
57
|
+
else if (isSource(e.name))
|
|
58
|
+
out.push(path.join(dir, e.name));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function discoverSources(input) {
|
|
62
|
+
const set = new Set();
|
|
63
|
+
const errors = [];
|
|
64
|
+
for (const p of input.paths ?? []) {
|
|
65
|
+
if (!fs.existsSync(p)) {
|
|
66
|
+
errors.push(`path not found: ${p}`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (fs.statSync(p).isDirectory()) {
|
|
70
|
+
const found = [];
|
|
71
|
+
walkDir(p, found);
|
|
72
|
+
found.forEach((f) => set.add(path.resolve(f)));
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
set.add(path.resolve(p));
|
|
76
|
+
}
|
|
77
|
+
for (const f of input.files ?? []) {
|
|
78
|
+
if (fs.existsSync(f))
|
|
79
|
+
set.add(path.resolve(f));
|
|
80
|
+
else
|
|
81
|
+
errors.push(`file not found: ${f}`);
|
|
82
|
+
}
|
|
83
|
+
if (input.changedSince) {
|
|
84
|
+
try {
|
|
85
|
+
(0, child_process_1.execFileSync)("git", ["diff", "--name-only", input.changedSince], { encoding: "utf8" })
|
|
86
|
+
.split("\n").map((s) => s.trim())
|
|
87
|
+
.filter((f) => f && isSource(f) && fs.existsSync(f))
|
|
88
|
+
.forEach((f) => set.add(path.resolve(f)));
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
errors.push(`--changed-since ${input.changedSince} failed: ${(0, util_1.errMsg)(e)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { files: [...set].sort(), errors };
|
|
95
|
+
}
|