@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 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;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};